Skip to content

fix(policy): capture and display policy signature timestamps - #3534

Closed
tofikwest wants to merge 3 commits into
mainfrom
tofik/cs-790-improvement-log-display-signed-date
Closed

fix(policy): capture and display policy signature timestamps#3534
tofikwest wants to merge 3 commits into
mainfrom
tofik/cs-790-improvement-log-display-signed-date

Conversation

@tofikwest

@tofikwest tofikwest commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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 auditLog entry alongside Policy.signedBy. Fixes the missing “when” data for SOC 2 and aligns with Linear CS-790.

  • New Features
    • Added acceptPolicyForMember to update Policy.signedBy and create an auditLog entry in one transaction, safely handling concurrent accepts.
    • Updated accept-policies and mark-policy-completed routes to use the helper and deduplicate policyIds; re-accepts are no-ops and keep the original time.
    • Added unit tests for timestamp capture, org scoping, duplicate IDs, auth guards, and overlap idempotency.
    • No backfill: existing signatures without timestamps remain unchanged.

Written for commit 9a58908. Summary will update on new commits.

Review in cubic

## 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 ✅
@linear

linear Bot commented Jul 29, 2026

Copy link
Copy Markdown

CS-790

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Jul 29, 2026 1:29pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Jul 29, 2026 1:29pm
portal Skipped Skipped Jul 29, 2026 1:29pm

Request Review

@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/portal/src/lib/policy-acceptance.ts
Comment thread apps/portal/src/app/api/portal/accept-policies/route.ts Outdated
where: { id: policy.id },
data: { signedBy: { push: member.id } },
}),
db.auditLog.create({

@cubic-dev-ai cubic-dev-ai Bot Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

@tofikwest

Copy link
Copy Markdown
Contributor Author

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).
→ This is a scope/feature-completeness observation, not a defect introduced or exposed by the diff — nothing in the changed code is incorrect or regressed by the absence of an export. The timestamped data the finding wants is already readable through an existing, permission-gated path: apps/api/src/audit/audit-log.controller.ts:16-128 exposes GET /v1/audit-logs with entityType/entityId filters (both accept comma-separated values, lines 55-70), pagination, orderBy: [{timestamp:'desc'},{id:'desc'}], org-scoped from the auth context rather than user input (line 54), guarded by HybridAuthGuard + PermissionGuard with @RequirePermission('app','read'). The frontend already consumes it for policies via apps/app/src/app/(app)/[orgId]/policies/[policyId]/hooks/useAuditLogs.ts (re-export of the shared useAuditLogs, documented as entityType='policy') and PolicyPageTabs.tsx, so the new entityType: 'policy' / description: 'accepted this policy' rows surface in the policy Activity tab without further work. policy is a valid AuditLogEntityType enum member (shared.prisma:50-65), so the writes are queryable. I did confirm there is no dedicated per-member 'signed on ' column or CSV export anywhere in apps/app (no signedAt/signedDate/acceptedAt/Signed on matches), so the reporting surface is genuinely thinner than a full CS-790 build-out — but asking this diff to also ship an export is a scope request for follow-up work, not a code defect in the reviewed change.

Address review findings.
@tofikwest

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review it

@vercel
vercel Bot temporarily deployed to Preview – app July 29, 2026 11:48 Inactive
@cubic-dev-ai

cubic-dev-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review it

@tofikwest I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 4 files

Confidence score: 3/5

  • In apps/portal/src/lib/policy-acceptance.ts, currentVersionId is 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,

@cubic-dev-ai cubic-dev-ai Bot Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant