Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions billing-receipt-privacy-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Billing Receipt Privacy Guard

This module adds a focused Revenue Infrastructure slice for SCIBASE issue #20. It validates customer-facing invoices, receipts, and payment-provider metadata before billing artifacts leave SCIBASE.

The guard detects private research project context, restricted dataset references, collaborator identifiers, grant-sensitive phrases, unsafe receipt identifiers, unsafe customer-facing envelope fields, malformed monetary or quantity fields, malformed line-item entries, unsafe line-item fields, and unsafe provider metadata, including nested provider metadata values and provider metadata key names. Missing receipt lists, line-item lists, and provider metadata are treated as empty billing evidence rather than crashing receipt review. Safe receipts remain deliverable, while unsafe receipts are held for finance review with redacted replacement identifiers, safe currency labels, replacement line items, metadata-key redaction handles, and deterministic audit evidence.

## Run

```bash
npm test
npm run demo
npm run video
npm run check
```

## Outputs

- `reports/receipt-privacy-packet.json`
- `reports/malformed-receipt-privacy-packet.json`
- `reports/malformed-line-item-privacy-packet.json`
- `reports/receipt-privacy-report.md`
- `reports/summary.svg`
- `reports/demo.mp4`

All data is synthetic. The module does not call payment processors, customer systems, private workspaces, institutional finance tools, or external APIs.
32 changes: 32 additions & 0 deletions billing-receipt-privacy-guard/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Acceptance Notes

This #20 slice focuses specifically on privacy-safe billing artifacts before invoices, receipts, and provider metadata leave SCIBASE.

It is not:

- a broad revenue infrastructure module
- a subscription entitlement or renewal guard
- a tax, procurement, or invoice-acceptance workflow
- a payment-rail failover or webhook entitlement verifier
- an analytics licensing export gate
- a revenue dispute, reconciliation, or credit-breakage ledger

Validation coverage:

- safe receipts are deliverable with only allowed provider metadata
- private research project context is removed from customer-facing receipt line items
- restricted dataset details are replaced with usage-category-safe wording
- receipt, invoice, and customer identifiers are redacted when they expose private context
- redacted receipt identifiers remain distinct for finance review correlation
- customer-facing currency labels are replaced with `XXX` when they carry restricted dataset context
- customer-facing totals, quantities, and line-item amounts are replaced with `null` when they carry restricted dataset context
- customer-facing totals, quantities, and line-item amounts are replaced with `null` when they are malformed or negative, even without private research text
- malformed line-item entries are held with `malformed-line-item` findings instead of crashing receipt review
- customer-facing line-item identifiers and units are redacted when they contain restricted dataset context
- missing provider metadata is treated as an empty provider packet instead of crashing receipt review
- missing receipt and line-item collections are treated as empty billing evidence instead of crashing receipt review
- unsafe provider metadata keys are removed before delivery
- unsafe provider metadata key names are redacted when the key itself carries restricted dataset context
- allowlisted provider metadata keys are still scanned when values are structured or nested
- customer copies retain useful totals, currency, usage categories, quantities, and units
- audit digests are deterministic and private-context free
150 changes: 150 additions & 0 deletions billing-receipt-privacy-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const fs = require('fs');
const path = require('path');
const { evaluateReceiptPrivacy, buildSampleBatch } = require('./index');

const reportsDir = path.join(__dirname, 'reports');
fs.mkdirSync(reportsDir, { recursive: true });

const result = evaluateReceiptPrivacy(buildSampleBatch());
const emptyResult = evaluateReceiptPrivacy({
batchId: 'billing-empty-review-20',
generatedAt: '2026-05-30T12:00:00Z'
});
const malformedResult = evaluateReceiptPrivacy({
batchId: 'billing-malformed-fields-review-20',
generatedAt: '2026-05-30T12:15:00Z',
receipts: [
{
id: 'receipt-malformed-numeric-fields',
invoiceId: 'inv-malformed-numeric-fields',
customerId: 'customer-lab-013',
currency: 'USD',
totalCents: 'free-form total',
providerMetadata: {
accountRef: 'acct-lab-013',
billingPeriod: '2026-05',
invoiceRef: 'inv-malformed-numeric-fields',
plan: 'lab-pro'
},
lineItems: [
{
id: 'line-malformed-quantity',
description: 'Lab Pro monthly subscription',
usageCategory: 'subscription',
quantity: 'one',
unit: 'month',
amountCents: -2500
}
]
}
]
});
const malformedLineItemResult = evaluateReceiptPrivacy({
batchId: 'billing-malformed-line-item-review-20',
generatedAt: '2026-05-31T08:45:00Z',
receipts: [
{
id: 'receipt-malformed-line-item',
invoiceId: 'inv-malformed-line-item',
customerId: 'customer-lab-014',
currency: 'USD',
totalCents: 19900,
providerMetadata: {
accountRef: 'acct-lab-014',
billingPeriod: '2026-05',
invoiceRef: 'inv-malformed-line-item',
plan: 'lab-pro'
},
lineItems: [null]
}
]
});

const packetPath = path.join(reportsDir, 'receipt-privacy-packet.json');
const emptyPacketPath = path.join(reportsDir, 'empty-receipt-privacy-packet.json');
const malformedPacketPath = path.join(reportsDir, 'malformed-receipt-privacy-packet.json');
const malformedLineItemPacketPath = path.join(reportsDir, 'malformed-line-item-privacy-packet.json');
const reportPath = path.join(reportsDir, 'receipt-privacy-report.md');
const svgPath = path.join(reportsDir, 'summary.svg');

fs.writeFileSync(packetPath, `${JSON.stringify(result, null, 2)}\n`);
fs.writeFileSync(emptyPacketPath, `${JSON.stringify(emptyResult, null, 2)}\n`);
fs.writeFileSync(malformedPacketPath, `${JSON.stringify(malformedResult, null, 2)}\n`);
fs.writeFileSync(malformedLineItemPacketPath, `${JSON.stringify(malformedLineItemResult, null, 2)}\n`);

const receipts = result.receipts
.map(
(receipt) =>
`- ${receipt.id}: ${receipt.decision}, findings: ${
receipt.findings.length > 0 ? receipt.findings.join(', ') : 'none'
}`
)
.join('\n');

const actions = result.remediationActions
.map((action) => `- ${action.id}: ${action.action} (${action.priority})`)
.join('\n');

const markdown = `# Billing Receipt Privacy Guard

Batch: ${result.batchId}
Generated: ${result.generatedAt}

## Summary

- Deliverable receipts: ${result.summary.deliverableReceipts}
- Held receipts: ${result.summary.heldReceipts}
- Remediation actions: ${result.summary.remediationActions}
- Total cents reviewed: ${result.summary.totalCentsReviewed}
- Audit digest: ${result.auditDigest}

## Receipt Decisions

${receipts}

## Remediation Actions

${actions}

## Sparse Billing Batch Guard

Empty or partially populated provider batches that omit receipt or line-item collections produce deterministic empty review evidence instead of runtime failures. The empty batch fixture reviewed ${emptyResult.receipts.length} receipts and generated ${emptyResult.remediationActions.length} remediation actions.

## Malformed Billing Field Guard

Receipts with non-numeric totals, quantities, or line-item amounts are held before delivery. The malformed fixture decision is ${malformedResult.receipts[0].decision}, and customer-facing numeric fields are redacted to ${malformedResult.receipts[0].customerCopy.totalCents}.

## Malformed Line Item Guard

Malformed line-item entries are held before delivery instead of crashing receipt review. The malformed line-item fixture decision is ${malformedLineItemResult.receipts[0].decision}, and the customer-facing line item id is ${malformedLineItemResult.receipts[0].customerCopy.lineItems[0].id}.

## Safety

All fixtures are synthetic. The guard does not call payment processors, customer systems, private workspaces, institutional finance tools, or external APIs.
`;

fs.writeFileSync(reportPath, markdown);

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="720" viewBox="0 0 1280 720">
<rect width="1280" height="720" fill="#102027"/>
<rect x="54" y="58" width="1172" height="604" rx="16" fill="#17313a" stroke="#9bd67a" stroke-width="4"/>
<text x="96" y="134" fill="#ffffff" font-family="Arial, sans-serif" font-size="42" font-weight="700">Billing Receipt Privacy Guard</text>
<text x="96" y="208" fill="#dff5d5" font-family="Arial, sans-serif" font-size="28">Deliverable receipts: ${result.summary.deliverableReceipts}</text>
<text x="96" y="258" fill="#dff5d5" font-family="Arial, sans-serif" font-size="28">Held receipts: ${result.summary.heldReceipts}</text>
<text x="96" y="308" fill="#dff5d5" font-family="Arial, sans-serif" font-size="28">Remediation actions: ${result.summary.remediationActions}</text>
<text x="96" y="380" fill="#ffffff" font-family="Arial, sans-serif" font-size="24">Checks: line-item text, provider metadata, restricted datasets, collaborator identifiers</text>
<text x="96" y="448" fill="#ffd37a" font-family="Arial, sans-serif" font-size="26">Private research context is replaced before receipts leave SCIBASE.</text>
<text x="96" y="574" fill="#a6d7c3" font-family="Arial, sans-serif" font-size="18">${result.auditDigest}</text>
</svg>
`;

fs.writeFileSync(svgPath, svg);

console.log(`Wrote ${path.relative(__dirname, packetPath)}`);
console.log(`Wrote ${path.relative(__dirname, emptyPacketPath)}`);
console.log(`Wrote ${path.relative(__dirname, malformedPacketPath)}`);
console.log(`Wrote ${path.relative(__dirname, malformedLineItemPacketPath)}`);
console.log(`Wrote ${path.relative(__dirname, reportPath)}`);
console.log(`Wrote ${path.relative(__dirname, svgPath)}`);
console.log(`Deliverable receipts: ${result.summary.deliverableReceipts}`);
console.log(`Held receipts: ${result.summary.heldReceipts}`);
Loading