Skip to content

[FEAT] Ordo — Liturgical Calendar Master Data #252

@kilip

Description

@kilip

Overview

Implement the ordo feature — a centralized liturgical calendar reference seeded from lagumisa.web.id and manageable by Parish Admins. This covers the full stack: DB schema, service layer, and CRUD UI in apps/dash.

Reference: docs/erd.md Section 2.5, docs/prd.md Section 6.5.


Scope

  • Drizzle schema + migration for ordo table
  • OrdoEntity, OrdoService, IOrdoRepository in packages/core and packages/db
  • Server Actions for CRUD
  • List and detail pages in apps/dash
  • Admin create/edit form
  • Read-only view for all authenticated users

1. Enums

Add to packages/core/src/entity/enums.ts:

export const CelebrationRank = {
  Solemnity: 'solemnity',
  Feast: 'feast',
  Memorial: 'memorial',
  Commemoration: 'commemoration',
  Feria: 'feria',
} as const
export type CelebrationRank = typeof CelebrationRank[keyof typeof CelebrationRank]

export const LiturgicalColor = {
  Purple: 'purple',
  White: 'white',
  Red: 'red',
  Green: 'green',
  Rose: 'rose',
  Black: 'black',
} as const
export type LiturgicalColor = typeof LiturgicalColor[keyof typeof LiturgicalColor]

export const OrdoSource = {
  Lagumisa: 'lagumisa',
  Manual: 'manual',
} as const
export type OrdoSource = typeof OrdoSource[keyof typeof OrdoSource]

2. DB Schema

Add to packages/db/src/schema/ordo.ts:

export const ordoTable = pgTable('ordo', {
  id: uuid('id').primaryKey().$defaultFn(() => v7()),
  date: date('date').notNull(),
  name: text('name').notNull(),
  rank: text('rank').notNull(),             // CelebrationRank
  color: text('color').notNull(),           // LiturgicalColor
  massLabel: text('mass_label'),
  readings: text('readings').array().notNull().default([]),
  songs: text('songs'),
  source: text('source').notNull().default('manual'),  // OrdoSource
  createdBy: text('created_by').references(() => usersTable.id),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
  deletedAt: timestamp('deleted_at'),
}, (t) => ({
  uniqDateMass: unique().on(t.date, t.massLabel).nullsNotDistinct(),
}))

Include Drizzle migration.


3. Core Layer (packages/core)

Entity — packages/core/src/entity/ordo.ts

export const OrdoEntity = z.object({
  id: z.string(),
  date: z.string(),           // ISO date string YYYY-MM-DD
  name: z.string().min(1),
  rank: z.nativeEnum(CelebrationRank),
  color: z.nativeEnum(LiturgicalColor),
  massLabel: z.string().nullable(),
  readings: z.array(z.string()),
  songs: z.string().nullable(),
  source: z.nativeEnum(OrdoSource),
  createdBy: z.string().nullable(),
  createdAt: z.date(),
  updatedAt: z.date(),
  deletedAt: z.date().nullable(),
})
export type Ordo = z.infer<typeof OrdoEntity>

Contract — packages/core/src/contract/ordo.ts

export interface IOrdoRepository {
  findById(id: string): Promise<Ordo | null>
  findByDate(date: string): Promise<Ordo[]>             // may return multiple (multi-mass days)
  findByDateRange(from: string, to: string): Promise<Ordo[]>
  findByMonth(year: number, month: number): Promise<Ordo[]>
  create(data: CreateOrdoDto): Promise<Ordo>
  update(id: string, data: UpdateOrdoDto): Promise<Ordo>
  delete(id: string): Promise<void>
}

DTOs

export type CreateOrdoDto = Omit<Ordo, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt'>
export type UpdateOrdoDto = Partial<Omit<Ordo, 'id' | 'source' | 'createdBy' | 'createdAt' | 'updatedAt' | 'deletedAt'>>

Service — packages/core/src/service/ordo.ts

export class OrdoService {
  constructor(private readonly repo: IOrdoRepository) {}

  async findByDate(date: string): Promise<Result<Ordo[]>>
  async findByMonth(year: number, month: number): Promise<Result<Ordo[]>>
  async create(data: CreateOrdoDto, ctx: AuthContext): Promise<Result<Ordo>>
  async update(id: string, data: UpdateOrdoDto, ctx: AuthContext): Promise<Result<Ordo>>
  async delete(id: string, ctx: AuthContext): Promise<Result<void>>
}

Permission rules:

  • create, update, delete → requires role: parish-admin or role: super-admin
  • Read operations → no AuthContext needed, accessible to all authenticated users

4. Repository (packages/db)

Implement OrdoRepository in packages/db/src/repository/ordo.ts — implements IOrdoRepository.

Include ILogger constructor injection following the pattern in docs/tdd.md Section 11.2.


5. Server Actions (apps/dash)

Location: apps/dash/src/features/ordo/actions/

  • getOrdoByDateAction(date: string): Promise<Result<Ordo[]>>
  • getOrdoByMonthAction(year: number, month: number): Promise<Result<Ordo[]>>
  • createOrdoAction(data: CreateOrdoDto): Promise<Result<Ordo>>
  • updateOrdoAction(id: string, data: UpdateOrdoDto): Promise<Result<Ordo>>
  • deleteOrdoAction(id: string): Promise<Result<void>>

All actions follow the Result<T> return convention from docs/tdd.md Section 4.4.


6. UI (apps/dash)

Routes

Route Access Description
/ordo All authenticated Monthly calendar view
/ordo/[date] All authenticated Day detail — all masses for a date
/ordo/new Parish Admin only Create new ordo entry
/ordo/[id]/edit Parish Admin only Edit ordo entry

Pages

/ordo — Monthly view

  • Month/year navigation
  • List of celebrations grouped by date
  • Color indicator per entry (liturgical color badge)
  • Link to day detail

/ordo/[date] — Day detail

  • Shows all masses for the date (if multi-mass)
  • Displays: name, rank, color, massLabel, readings list, songs
  • Edit/Delete buttons visible to Parish Admins only

/ordo/new and /ordo/[id]/edit — Form

  • Fields: date, name, rank (select), color (select), massLabel (optional), readings (dynamic list), songs (textarea)
  • source auto-set to manual on create/edit via UI
  • Submit → redirect to /ordo/[date]

7. Service Registration

Register OrdoService in apps/dash/src/lib/services.ts (the composition root):

import { OrdoRepository } from '@domus/db/repository/ordo'
import { OrdoService } from '@domus/core/service/ordo'

const ordoRepository = new OrdoRepository(db, logger)
export const ordoService = new OrdoService(ordoRepository)

Acceptance Criteria

  • ordo table migration applied successfully
  • OrdoEntity, IOrdoRepository, OrdoService implemented in packages/core
  • OrdoRepository implemented in packages/db with ILogger injection
  • OrdoService registered in apps/dash/src/lib/services.ts
  • All 5 Server Actions implemented and follow Result<T> convention
  • create/update/delete actions enforce parish-admin or super-admin role
  • Monthly view renders correctly with liturgical color indicators
  • Day detail shows all masses for a given date
  • Create/edit form saves correctly with source: 'manual'
  • Scraper-seeded entries (source: 'lagumisa') are editable by admin but not overwritten by future scraper runs
  • Soft delete works correctly — deleted entries not shown in list

Files to Create / Modify

packages/core/src/
  entity/ordo.ts                    # new
  entity/enums.ts                   # extend: CelebrationRank, LiturgicalColor, OrdoSource
  contract/ordo.ts                  # new
  service/ordo.ts                   # new

packages/db/src/
  schema/ordo.ts                    # new
  repository/ordo.ts                # new
  drizzle/<timestamp>_add_ordo.sql  # new migration

apps/dash/src/
  lib/services.ts                   # register OrdoService
  features/ordo/
    actions/get-ordo-by-date.ts     # new
    actions/get-ordo-by-month.ts    # new
    actions/create-ordo.ts          # new
    actions/update-ordo.ts          # new
    actions/delete-ordo.ts          # new
  pages/ordo/
    OrdoListPage.tsx                # new
    OrdoDayPage.tsx                 # new
    OrdoFormPage.tsx                # new

app/(dash)/
  ordo/page.tsx                     # thin export
  ordo/[date]/page.tsx              # thin export
  ordo/new/page.tsx                 # thin export
  ordo/[id]/edit/page.tsx           # thin export

References

  • Entity & table definition: docs/erd.md Section 2.5
  • Feature spec: docs/prd.md Section 6.5
  • Service pattern: docs/tdd.md Section 4.7
  • Server Action convention: docs/tdd.md Section 4.4
  • Composition root: apps/dash/src/lib/services.ts
  • Thin export convention: docs/tdd.md Section 3.1

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Ready

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions