From eaad16583b137bff8105a35e54af59cac552636c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 16:43:16 +0000 Subject: [PATCH] =?UTF-8?q?fix(hr):=20correct=20document-expiry=20date=20f?= =?UTF-8?q?ormulas=20=E2=80=94=20CEL=20has=20no=20Timestamp+int?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `hr_document.is_expiring_soon` and `expiry_status` used `today() + 30` / `today() + 60` to build the reminder windows. The formula engine is CEL, where dates are Timestamps and there is no `Timestamp + int` operator — so `today() + 30` evaluated to null and every comparison against it silently failed. Net effect: `is_expiring_soon` was always false and `expiry_status` never returned the `expiring_30d` / `expiring_60d` bands (only expired / valid / none), so the expiry-reminder flow had nothing to fire on. Fix — use the `daysFromNow(n)` builtin (which returns the Timestamp n days out) instead of `today() + n`: expires_at <= daysFromNow(30) // was (today() + 30) expires_at <= daysFromNow(60) // was (today() + 60) Boot-verified against a fresh dev server (@objectstack 15.1.1, 0 errors) — the seeded documents now band correctly: - expires in ~15d → is_expiring_soon = true, expiry_status = expiring_30d - expires next year → is_expiring_soon = false, expiry_status = valid - expired 10d ago → is_expired = true, expiry_status = expired - no expiry date → expiry_status = none Note: `hr_employee.tenure_years` has a related but distinct defect (`floor((today() - hire_date) / 365)` — CEL has neither `Timestamp - Timestamp` → number nor a `floor` builtin, and the intended `daysBetween()` primitive is declared in @objectstack/formula's type list but not wired into the runtime, so it returns null). That one can't be fixed at the template level without a working date-diff builtin, so it is intentionally left for a framework follow-up rather than worked around with an unreadable bucketed ternary. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BZguyAaQbyUpwMZ2gMLaAP --- packages/hr/src/objects/hr_document.object.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/hr/src/objects/hr_document.object.ts b/packages/hr/src/objects/hr_document.object.ts index 54e4e46..360b401 100644 --- a/packages/hr/src/objects/hr_document.object.ts +++ b/packages/hr/src/objects/hr_document.object.ts @@ -48,7 +48,10 @@ export const EmployeeDocument = ObjectSchema.create({ }), is_expiring_soon: Field.formula({ label: 'Expiring Soon?', - expression: F`record.expires_at != null && record.expires_at >= today() && record.expires_at <= (today() + 30)`, + // `daysFromNow(30)`, not `today() + 30`: the CEL formula engine has no + // `Timestamp + int` operator, so `today() + 30` evaluates to null and the + // whole comparison silently fails. Day offsets go through the builtin. + expression: F`record.expires_at != null && record.expires_at >= today() && record.expires_at <= daysFromNow(30)`, }), is_expired: Field.formula({ label: 'Expired?', @@ -57,7 +60,9 @@ export const EmployeeDocument = ObjectSchema.create({ expiry_status: Field.formula({ label: 'Expiry Status', description: 'Single severity band for sorting/colour-coding the document list.', - expression: F`record.expires_at == null ? "none" : (record.expires_at < today() ? "expired" : (record.expires_at <= (today() + 30) ? "expiring_30d" : (record.expires_at <= (today() + 60) ? "expiring_60d" : "valid")))`, + // `daysFromNow(N)`, not `today() + N`: CEL has no `Timestamp + int` operator + // (see is_expiring_soon above), so the offset bands must use the builtin. + expression: F`record.expires_at == null ? "none" : (record.expires_at < today() ? "expired" : (record.expires_at <= daysFromNow(30) ? "expiring_30d" : (record.expires_at <= daysFromNow(60) ? "expiring_60d" : "valid")))`, }), notes: Field.markdown({ label: 'Notes' }), },