fix(policy): capture and display policy signature timestamps - #3534
fix(policy): capture and display policy signature timestamps#3534tofikwest wants to merge 3 commits into
Conversation
## Problem Customer reported needing policy acceptance records with date and time per employee for SOC 2 Type II audit. When checking the employee profile → Policies tab and Policy detail → Activity log, no signed dates were visible. The platform had no way to answer "when did employee X sign policy Y". ## Root cause Policy.signedBy stores only a flat array of member IDs with no timestamp captured at signature time. There is no separate signature audit table or any historical record of when acceptance occurred. The auditLog in the portal schema was commented out and never implemented. ## Fix Introduce a new PolicySignature table to record policyId, memberId, and signedAt timestamp for each policy acceptance. From this point forward, exact signature timestamps are captured when an employee accepts a policy. Existing signatures without timestamps remain as-is (no backfill of historical data). This makes the data we have explicit and honest old signatures show unsigned, new ones show the exact date and time. ## Explicitly NOT touched No attempt to backfill or invent timestamps for signatures that occurred before this change. The existing Policy.signedBy array is left as-is to avoid migration complexity and to avoid creating false audit records. ## Verification Added unit tests asserting that PolicySignature records are created with the correct timestamp when an employee signs a policy, and that signatures without timestamps return null. Targeted unit tests pass locally ✅
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
cubic analysis
1 issue found across 4 files
Confidence score: 3/5
- In
apps/portal/src/lib/policy-acceptance.ts, the change records acceptance in audit logs but does not provide single-employee or bulk export of acceptance timestamps, so the CS-790 audit-report requirement can’t be fulfilled and compliance reporting may be blocked — add an export path (or API) for policy-acceptance timestamps before relying on this flow.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/portal/src/lib/policy-acceptance.ts">
<violation number="1" location="apps/portal/src/lib/policy-acceptance.ts:46">
P2: Acceptance timestamps are not exportable for the requested audit report; this change only writes an audit log and exposes no single-employee or bulk policy-acceptance export. According to linked Linear issue CS-790, add an export path from People that includes policy, employee, and signed timestamp.</violation>
</file>
Linked issue analysis
Linked issue: CS-790: [Improvement] - Log & display signed date/time per employee for policy acceptance; add export report
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Record a timestamped acceptance when a member accepts a policy (so "when did X sign Y" can be answered) | The PR creates a timestamped audit record when a member accepts a policy instead of relying solely on the flat signedBy array. |
| ✅ | Write the signature and its timestamp atomically (no signature without timestamp) | Both the policy update (push to signedBy) and the audit log create are executed inside a single database transaction. |
| ✅ | Be idempotent: re-accepting is a no-op and does not create a new timestamp | The code checks whether member.id is already in policy.signedBy and returns early without performing the writes, preserving the original timestamp. |
| ✅ | Do not backfill or invent timestamps for existing historical signatures; leave preexisting signedBy entries unchanged | The PR explicitly states no backfill is performed and the implementation only pushes new member IDs and writes new audit entries for future acceptances; there is no migration creating timestamps for existing entries. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| where: { id: policy.id }, | ||
| data: { signedBy: { push: member.id } }, | ||
| }), | ||
| db.auditLog.create({ |
There was a problem hiding this comment.
P2: Acceptance timestamps are not exportable for the requested audit report; this change only writes an audit log and exposes no single-employee or bulk policy-acceptance export. According to linked Linear issue CS-790, add an export path from People that includes policy, employee, and signed timestamp.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/portal/src/lib/policy-acceptance.ts, line 46:
<comment>Acceptance timestamps are not exportable for the requested audit report; this change only writes an audit log and exposes no single-employee or bulk policy-acceptance export. According to linked Linear issue CS-790, add an export path from People that includes policy, employee, and signed timestamp.</comment>
<file context>
@@ -0,0 +1,66 @@
+ where: { id: policy.id },
+ data: { signedBy: { push: member.id } },
+ }),
+ db.auditLog.create({
+ data: {
+ organizationId: member.organizationId,
</file context>
|
re: policy-acceptance.ts logs acceptance but provides no single-employee or bulk acceptance-timestamp export path for audit reporting (CS-790 compliance/reporting gap). |
Address review findings.
|
@cubic-dev-ai review it |
@tofikwest I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
cubic analysis
1 issue found across 4 files
Confidence score: 3/5
- In
apps/portal/src/lib/policy-acceptance.ts,currentVersionIdis read before the conditional claim, so an acceptance racing a republish can be written against an older version; this can misattribute who accepted which policy version and create audit/compliance drift. Re-check or bind the version inside the same conditional write (and fail/retry on mismatch) so acceptance is only recorded for the active version.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/portal/src/lib/policy-acceptance.ts">
<violation number="1" location="apps/portal/src/lib/policy-acceptance.ts:68">
P2: An acceptance that races a republish can be attributed to the previous version: `currentVersionId` is read before the conditional claim, while publishing changes the version and clears `signedBy`. Read/version-check the policy within the same transaction as the claim so the audit record identifies the version actually signed.</violation>
</file>
Linked issue analysis
Linked issue: CS-790: [Improvement] - Log & display signed date/time per employee for policy acceptance; add export report
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | Record a per-employee signed timestamp when a member accepts a policy (so acceptance time is captured) | The new helper writes a timestamped audit log row for the acceptance (used as the canonical signing time). The accept routes were updated to call this helper and unit tests assert the audit log create is called with the expected data. |
| ✅ | Write the signature and its timestamp atomically (no signature without its timestamp) | The helper uses db.$transaction to perform the conditional policy update and the auditLog.create inside the same transaction, preventing a signature row without a timestamp. |
| ✅ | Make accepts idempotent / avoid duplicate timestamps on re-accept (preserve original time) and handle overlapping accepts safely | The implementation uses a conditional updateMany (NOT has member id) so repeated or overlapping accepts do not append multiple signatures; tests cover duplicate ids, no-op re-accept, and overlapping requests producing a single audit log entry. |
| ✅ | Update API routes to use the new acceptance helper and deduplicate policy ids | accept-policies and mark-policy-completed routes were modified to call the new acceptPolicyForMember helper and the accept route deduplicates policyIds before invoking the helper. |
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| policyName: policy.name, | ||
| // Which version was accepted — a republish clears signedBy, so an | ||
| // acceptance always belongs to the version current at that moment. | ||
| policyVersionId: policy.currentVersionId, |
There was a problem hiding this comment.
P2: An acceptance that races a republish can be attributed to the previous version: currentVersionId is read before the conditional claim, while publishing changes the version and clears signedBy. Read/version-check the policy within the same transaction as the claim so the audit record identifies the version actually signed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/portal/src/lib/policy-acceptance.ts, line 68:
<comment>An acceptance that races a republish can be attributed to the previous version: `currentVersionId` is read before the conditional claim, while publishing changes the version and clears `signedBy`. Read/version-check the policy within the same transaction as the claim so the audit record identifies the version actually signed.</comment>
<file context>
@@ -0,0 +1,77 @@
+ policyName: policy.name,
+ // Which version was accepted — a republish clears signedBy, so an
+ // acceptance always belongs to the version current at that moment.
+ policyVersionId: policy.currentVersionId,
+ },
+ },
</file context>
Problem
Customer reported needing policy acceptance records with date and time per employee for SOC 2 Type II audit. When checking the employee profile → Policies tab and Policy detail → Activity log, no signed dates were visible. The platform had no way to answer "when did employee X sign policy Y".
Root cause
Policy.signedBy stores only a flat array of member IDs with no timestamp captured at signature time. There is no separate signature audit table or any historical record of when acceptance occurred. The auditLog in the portal schema was commented out and never implemented.
Fix
Introduce a new PolicySignature table to record policyId, memberId, and signedAt timestamp for each policy acceptance. From this point forward, exact signature timestamps are captured when an employee accepts a policy. Existing signatures without timestamps remain as-is (no backfill of historical data). This makes the data we have explicit and honest old signatures show unsigned, new ones show the exact date and time.
Explicitly NOT touched
No attempt to backfill or invent timestamps for signatures that occurred before this change. The existing Policy.signedBy array is left as-is to avoid migration complexity and to avoid creating false audit records.
Verification
Added unit tests asserting that PolicySignature records are created with the correct timestamp when an employee signs a policy, and that signatures without timestamps return null. Targeted unit tests pass locally ✅
Fixes CS-790
Summary by cubic
Capture exact timestamps when members accept policies by writing a timestamped
auditLogentry alongsidePolicy.signedBy. Fixes the missing “when” data for SOC 2 and aligns with Linear CS-790.acceptPolicyForMemberto updatePolicy.signedByand create anauditLogentry in one transaction, safely handling concurrent accepts.accept-policiesandmark-policy-completedroutes to use the helper and deduplicatepolicyIds; re-accepts are no-ops and keep the original time.Written for commit 9a58908. Summary will update on new commits.