skills/security-compliance/financial-audit-trail/SKILL.md
Build immutable audit trails for all financial transactions with user attribution, change logging, tamper detection, and compliance-ready export for external audits
npx skillsauth add finsilabs/awesome-ecommerce-skills financial-audit-trailInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A financial audit trail records every change to financial data — orders, payments, refunds, manual price adjustments, and invoice approvals — with who made the change, when, from what IP, and what the record looked like before and after. This infrastructure is required for PCI-DSS logging compliance, SOX ITGC evidence, GDPR erasure audit trails, and financial statement audits. Every major e-commerce platform generates some audit history; the gap is usually in completeness, manual change attribution, and export format for auditors.
| Platform | Built-in Audit Capability | What You Need to Add | |----------|--------------------------|---------------------| | Shopify | Order timeline records all events (created, payment captured, refunded, edited); admin action attribution is limited | Export timeline data via Admin API; for admin changes, enable Staff activity logging under Settings | | WooCommerce | Order notes show customer-visible actions; system notes show some status changes | Install WooCommerce Admin Audit Log or Simple History plugin for full admin action tracking | | BigCommerce | Store logs in Advanced Settings → Store Logs for system events; order history available via API | For admin action attribution, export via API and combine with server access logs | | Custom / Headless | Nothing built in | Must build — see Custom section |
Enabling and accessing audit data:
GET /admin/api/2024-04/events.json returns all store eventsverb (confirmed, placed, edited, refunded, etc.) and subject_type (Order, Refund, Customer)Export for auditors:
// Fetch all financial events for a date range
const events = await shopify.event.list({
verb: 'confirmed,placed,refunded,voided',
created_at_min: '2026-01-01T00:00:00Z',
created_at_max: '2026-12-31T23:59:59Z',
limit: 250,
});
Staff action tracking (Shopify Plus):
WooCommerce order notes provide some audit history, but do not track which admin user made a change. Install a dedicated audit plugin.
Simple History plugin (free, recommended):
WooCommerce Admin Audit Log (premium, ~$50/year):
Manual order edit tracking: For any manual financial change (discount applied, total adjusted), add an Order Note (internal, not visible to customer) documenting:
This is the minimum acceptable evidence for an auditor when automated logging is not in place.
Accessing store logs:
Order history via API: Use the BigCommerce Orders API to extract a complete order history with status transitions:
// Get all orders modified in a date range
const orders = await bigcommerce.get('/v2/orders', {
min_date_modified: '2026-01-01T00:00:00+00:00',
max_date_modified: '2026-12-31T23:59:59+00:00',
status_id: '', // all statuses
limit: 250,
});
Combine with the Order Transactions API (/v2/orders/{id}/transactions) to get payment captures, refunds, and voids.
For custom storefronts, implement an append-only audit log that records every financial mutation. The key design requirements are: immutable (app role has no UPDATE or DELETE), includes before/after state, and supports tamper detection.
CREATE TABLE financial_audit_events (
id UUID NOT NULL DEFAULT gen_random_uuid(),
seq BIGSERIAL NOT NULL, -- Monotonic — gap detection
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
event_type VARCHAR(64) NOT NULL, -- 'order.total_changed', 'refund.issued'
aggregate_type VARCHAR(32) NOT NULL, -- 'order', 'payment', 'refund'
aggregate_id VARCHAR(128) NOT NULL,
actor_id VARCHAR(128) NOT NULL, -- user ID or 'system'
actor_role VARCHAR(64),
actor_ip INET,
before_state JSONB, -- Snapshot before change
after_state JSONB, -- Snapshot after change
delta JSONB, -- Only changed fields
correlation_id UUID, -- Tie events in same request
hash VARCHAR(64), -- SHA-256 for tamper detection
PRIMARY KEY (id)
);
-- Grant INSERT and SELECT only — never UPDATE or DELETE
GRANT INSERT, SELECT ON financial_audit_events TO app_role;
REVOKE UPDATE, DELETE ON financial_audit_events FROM app_role;
CREATE INDEX idx_fae_aggregate ON financial_audit_events (aggregate_type, aggregate_id, occurred_at DESC);
CREATE INDEX idx_fae_actor ON financial_audit_events (actor_id, occurred_at DESC);
CREATE INDEX idx_fae_seq ON financial_audit_events (seq);
Audit logger with tamper-detection hashing:
import { createHash } from 'crypto';
async function recordAuditEvent(input: {
eventType: string;
aggregateType: string;
aggregateId: string;
actorId: string;
actorRole?: string;
actorIp?: string;
beforeState?: object | null;
afterState?: object | null;
correlationId?: string;
}): Promise<void> {
const prevEvent = await db.financialAuditEvents.findFirst({
where: { aggregate_type: input.aggregateType, aggregate_id: input.aggregateId },
orderBy: { seq: 'desc' },
select: { seq: true, hash: true },
});
const occurredAt = new Date().toISOString();
const hashInput = [
String(prevEvent?.seq ?? 0),
occurredAt,
input.eventType,
input.aggregateId,
input.actorId,
JSON.stringify(input.afterState ?? null),
].join('|');
const hash = createHash('sha256').update(hashInput).digest('hex');
await db.financialAuditEvents.insert({
occurred_at: occurredAt,
event_type: input.eventType,
aggregate_type: input.aggregateType,
aggregate_id: input.aggregateId,
actor_id: input.actorId,
actor_role: input.actorRole ?? null,
actor_ip: input.actorIp ?? null,
before_state: input.beforeState ?? null,
after_state: input.afterState ?? null,
correlation_id: input.correlationId ?? null,
hash,
prev_hash: prevEvent?.hash ?? null,
});
}
Export for auditors:
async function exportAuditTrail(from: Date, to: Date, format: 'json' | 'csv'): Promise<Buffer> {
const events = await db.financialAuditEvents.findAll({
where: { occurred_at: { gte: from, lte: to } },
orderBy: { seq: 'asc' },
});
if (format === 'json') return Buffer.from(JSON.stringify(events, null, 2));
// CSV for auditor delivery
const rows = events.map(e => ({
'Date/Time': e.occurred_at,
'Event Type': e.event_type,
'Record Type': e.aggregate_type,
'Record ID': e.aggregate_id,
'Actor': e.actor_id,
'Actor Role': e.actor_role ?? '',
'IP Address': e.actor_ip ?? '',
'Before': e.before_state ? JSON.stringify(e.before_state) : '',
'After': e.after_state ? JSON.stringify(e.after_state) : '',
'Delta': e.delta ? JSON.stringify(e.delta) : '',
}));
return buildCsv(rows);
}
before_state and after_state on every mutation — storing only a delta is not enough for compliance; auditors need to reconstruct the full state of a record at any point in timecorrelation_id from the HTTP request — if a single API request creates multiple audit events, a shared correlation_id lets you reconstruct the full causal chain| Problem | Solution |
|---------|----------|
| Audit events missing because developers call db.update() directly | The audit logger must be called in the service layer, not the controller; add a code review checklist item that flags direct db.update calls on financial tables |
| before_state is null because the developer only captures state after the change | Fetch and snapshot the record BEFORE the mutation inside the same database transaction |
| Audit table grows to hundreds of millions of rows, slowing queries | Partition the table by occurred_at (monthly partitions); keep 12 months on hot storage, archive older partitions to S3 + Athena |
| An attacker who compromises the app role can delete audit rows | Revoke DELETE from all roles; consider a secondary write-only log stream to an external service (CloudWatch Logs, Datadog) |
| Compliance export takes hours to generate | Pre-build indexed views for common audit report patterns; ensure the occurred_at index is used in range queries |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion