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
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
Overview
Implement the
ordofeature — 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 inapps/dash.Reference:
docs/erd.mdSection 2.5,docs/prd.mdSection 6.5.Scope
ordotableOrdoEntity,OrdoService,IOrdoRepositoryinpackages/coreandpackages/dbapps/dash1. Enums
Add to
packages/core/src/entity/enums.ts:2. DB Schema
Add to
packages/db/src/schema/ordo.ts:Include Drizzle migration.
3. Core Layer (
packages/core)Entity —
packages/core/src/entity/ordo.tsContract —
packages/core/src/contract/ordo.tsDTOs
Service —
packages/core/src/service/ordo.tsPermission rules:
create,update,delete→ requiresrole: parish-adminorrole: super-adminAuthContextneeded, accessible to all authenticated users4. Repository (
packages/db)Implement
OrdoRepositoryinpackages/db/src/repository/ordo.ts— implementsIOrdoRepository.Include
ILoggerconstructor injection following the pattern indocs/tdd.mdSection 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 fromdocs/tdd.mdSection 4.4.6. UI (
apps/dash)Routes
/ordo/ordo/[date]/ordo/new/ordo/[id]/editPages
/ordo— Monthly view/ordo/[date]— Day detail/ordo/newand/ordo/[id]/edit— Formsourceauto-set tomanualon create/edit via UI/ordo/[date]7. Service Registration
Register
OrdoServiceinapps/dash/src/lib/services.ts(the composition root):Acceptance Criteria
ordotable migration applied successfullyOrdoEntity,IOrdoRepository,OrdoServiceimplemented inpackages/coreOrdoRepositoryimplemented inpackages/dbwithILoggerinjectionOrdoServiceregistered inapps/dash/src/lib/services.tsResult<T>conventioncreate/update/deleteactions enforceparish-adminorsuper-adminrolesource: 'manual'source: 'lagumisa') are editable by admin but not overwritten by future scraper runsFiles to Create / Modify
References
docs/erd.mdSection 2.5docs/prd.mdSection 6.5docs/tdd.mdSection 4.7docs/tdd.mdSection 4.4apps/dash/src/lib/services.tsdocs/tdd.mdSection 3.1