diff --git a/billing-receipt-privacy-guard/README.md b/billing-receipt-privacy-guard/README.md
new file mode 100644
index 00000000..5aa2da20
--- /dev/null
+++ b/billing-receipt-privacy-guard/README.md
@@ -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.
diff --git a/billing-receipt-privacy-guard/acceptance-notes.md b/billing-receipt-privacy-guard/acceptance-notes.md
new file mode 100644
index 00000000..275863f2
--- /dev/null
+++ b/billing-receipt-privacy-guard/acceptance-notes.md
@@ -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
diff --git a/billing-receipt-privacy-guard/demo.js b/billing-receipt-privacy-guard/demo.js
new file mode 100644
index 00000000..ac1e4347
--- /dev/null
+++ b/billing-receipt-privacy-guard/demo.js
@@ -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 = `
+`;
+
+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}`);
diff --git a/billing-receipt-privacy-guard/index.js b/billing-receipt-privacy-guard/index.js
new file mode 100644
index 00000000..1a344b1f
--- /dev/null
+++ b/billing-receipt-privacy-guard/index.js
@@ -0,0 +1,409 @@
+const crypto = require('crypto');
+
+const SAFE_METADATA_KEYS = new Set(['accountRef', 'billingPeriod', 'invoiceRef', 'plan']);
+
+const PRIVATE_PATTERNS = [
+ {
+ id: 'private-research-context',
+ pattern: /(alzheimer|single-cell|patient cohort|clinical trial|embargoed|irb)/i
+ },
+ {
+ id: 'restricted-dataset-reference',
+ pattern: /(gse-private|dbgap|controlled-access|restricted dataset|participant)/i
+ },
+ {
+ id: 'collaborator-identifier',
+ pattern: /(@|orcid|collaborator|researcher handle)/i
+ },
+ {
+ id: 'grant-sensitive-context',
+ pattern: /(grant confidential|sponsor confidential|unannounced award)/i
+ }
+];
+
+function stableStringify(value) {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(',')}]`;
+ }
+
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`;
+ }
+
+ return JSON.stringify(value);
+}
+
+function digest(value) {
+ return `sha256:${crypto.createHash('sha256').update(stableStringify(value)).digest('hex')}`;
+}
+
+function findingsForText(text) {
+ return PRIVATE_PATTERNS.filter((item) => item.pattern.test(text)).map((item) => item.id);
+}
+
+function evidenceList(value) {
+ return Array.isArray(value) ? value : [];
+}
+
+function privacyText(value) {
+ if (value === undefined || value === null) {
+ return '';
+ }
+
+ return metadataValueText(value);
+}
+
+function hasPrivateContext(value) {
+ return findingsForText(privacyText(value)).length > 0;
+}
+
+function isRecord(value) {
+ return value && typeof value === 'object' && !Array.isArray(value);
+}
+
+function lineItemPrivacyFindings(lineItem) {
+ return Array.from(new Set([
+ ...findingsForText(
+ stableStringify({
+ id: lineItem.id,
+ description: lineItem.description,
+ projectRef: lineItem.projectRef,
+ quantity: lineItem.quantity,
+ usageCategory: lineItem.usageCategory,
+ unit: lineItem.unit,
+ amountCents: lineItem.amountCents
+ })
+ ),
+ ...numericFieldFindings(lineItem.quantity, 'quantity'),
+ ...numericFieldFindings(lineItem.amountCents, 'cents')
+ ])).sort();
+}
+
+function categoryDescription(lineItem) {
+ if (lineItem.usageCategory === 'ai-compute') {
+ return 'AI compute usage for restricted research workspace';
+ }
+
+ if (lineItem.usageCategory === 'storage') {
+ return 'Restricted dataset storage and processing';
+ }
+
+ if (lineItem.usageCategory === 'analytics-license') {
+ return 'Analytics licensing service';
+ }
+
+ return 'Research platform service';
+}
+
+function sanitizeLineItem(lineItem, index) {
+ if (!isRecord(lineItem)) {
+ return {
+ id: `line-malformed-${index + 1}`,
+ usageCategory: 'billing-line-repair',
+ quantity: null,
+ unit: 'usage-unit',
+ amountCents: null,
+ description: 'Malformed billing line item requires finance repair',
+ findings: ['malformed-line-item']
+ };
+ }
+
+ const findings = lineItemPrivacyFindings(lineItem);
+ const description = findings.length > 0 ? categoryDescription(lineItem) : lineItem.description;
+ const id = hasPrivateContext(lineItem.id) ? `line-redacted-${index + 1}` : lineItem.id;
+ const usageCategory = hasPrivateContext(lineItem.usageCategory)
+ ? 'research-platform-service'
+ : lineItem.usageCategory;
+ const unit = hasPrivateContext(lineItem.unit) ? 'usage-unit' : lineItem.unit;
+
+ return {
+ id,
+ usageCategory,
+ quantity: sanitizeCustomerNumber(lineItem.quantity, 'quantity'),
+ unit,
+ amountCents: sanitizeCustomerNumber(lineItem.amountCents, 'cents'),
+ description,
+ findings
+ };
+}
+
+function receiptIdentifierFindings(receipt) {
+ return findingsForText(stableStringify({
+ id: receipt.id,
+ invoiceId: receipt.invoiceId,
+ customerId: receipt.customerId
+ }));
+}
+
+function receiptEnvelopeFindings(receipt) {
+ return Array.from(new Set([
+ ...findingsForText(stableStringify({
+ currency: receipt.currency,
+ totalCents: receipt.totalCents
+ })),
+ ...numericFieldFindings(receipt.totalCents, 'cents')
+ ])).sort();
+}
+
+function sanitizeIdentifier(value, fallback) {
+ return hasPrivateContext(value) ? fallback : value;
+}
+
+function sanitizeCurrency(value) {
+ return hasPrivateContext(value) ? 'XXX' : value;
+}
+
+function sanitizeCustomerNumber(value, kind = 'number') {
+ return hasPrivateContext(value) || numericFieldFindings(value, kind).length > 0 ? null : value;
+}
+
+function numericFieldFindings(value, kind) {
+ if (kind === 'cents') {
+ return Number.isInteger(value) && value >= 0 ? [] : ['invalid-billing-amount'];
+ }
+
+ if (kind === 'quantity') {
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? [] : ['invalid-billing-quantity'];
+ }
+
+ return typeof value === 'number' && Number.isFinite(value) ? [] : ['invalid-billing-number'];
+}
+
+function sanitizeMetadata(metadata = {}) {
+ const safe = {};
+ const removedKeys = [];
+ const findings = [];
+ let redactedKeyCount = 0;
+
+ for (const [key, value] of Object.entries(metadata)) {
+ const keyFindings = findingsForText(key);
+ const removedKey = keyFindings.length > 0
+ ? `metadata-key-redacted-${++redactedKeyCount}`
+ : key;
+
+ if (!SAFE_METADATA_KEYS.has(key)) {
+ removedKeys.push(removedKey);
+ findings.push('unsafe-provider-metadata');
+ findings.push(...keyFindings);
+ continue;
+ }
+
+ const textFindings = findingsForText(metadataValueText(value));
+ if (textFindings.length > 0) {
+ removedKeys.push(removedKey);
+ findings.push(...textFindings);
+ continue;
+ }
+
+ safe[key] = value;
+ }
+
+ return {
+ safe,
+ removedKeys: Array.from(new Set(removedKeys)).sort(),
+ findings: Array.from(new Set(findings)).sort()
+ };
+}
+
+function metadataValueText(value) {
+ if (value && typeof value === 'object') {
+ return stableStringify(value);
+ }
+
+ return String(value);
+}
+
+function evaluateReceipt(receipt, index) {
+ const redactedLineItems = evidenceList(receipt.lineItems).map((lineItem, index) => sanitizeLineItem(lineItem, index));
+ const lineFindings = redactedLineItems.flatMap((lineItem) => lineItem.findings);
+ const identifierFindings = receiptIdentifierFindings(receipt);
+ const envelopeFindings = receiptEnvelopeFindings(receipt);
+ const metadata = sanitizeMetadata(receipt.providerMetadata);
+ const findings = Array.from(new Set([...lineFindings, ...identifierFindings, ...envelopeFindings, ...metadata.findings])).sort();
+ const decision = findings.length > 0 ? 'hold-for-finance-review' : 'deliver-receipt';
+ const safeReceiptId = sanitizeIdentifier(receipt.id, `receipt-redacted-${index + 1}`);
+ const safeInvoiceId = sanitizeIdentifier(receipt.invoiceId, `invoice-redacted-${index + 1}`);
+ const safeCustomerId = sanitizeIdentifier(receipt.customerId, `customer-redacted-${index + 1}`);
+ const safeCurrency = sanitizeCurrency(receipt.currency);
+ const safeTotalCents = sanitizeCustomerNumber(receipt.totalCents, 'cents');
+
+ const customerCopy = {
+ receiptId: safeReceiptId,
+ customerId: safeCustomerId,
+ currency: safeCurrency,
+ totalCents: safeTotalCents,
+ lineItems: redactedLineItems.map((lineItem) => ({
+ id: lineItem.id,
+ description: lineItem.description,
+ usageCategory: lineItem.usageCategory,
+ quantity: lineItem.quantity,
+ unit: lineItem.unit,
+ amountCents: lineItem.amountCents
+ }))
+ };
+
+ return {
+ id: safeReceiptId,
+ invoiceId: safeInvoiceId,
+ customerId: safeCustomerId,
+ decision,
+ findings,
+ removedMetadataKeys: metadata.removedKeys,
+ providerMetadata: metadata.safe,
+ redactedLineItems: redactedLineItems.map((lineItem) => ({
+ id: lineItem.id,
+ description: lineItem.description,
+ usageCategory: lineItem.usageCategory,
+ findings: lineItem.findings
+ })),
+ customerCopy,
+ auditDigest: digest({
+ id: receipt.id,
+ invoiceId: receipt.invoiceId,
+ decision,
+ findings,
+ providerMetadata: metadata.safe,
+ customerCopy
+ })
+ };
+}
+
+function remediationAction(receipt) {
+ if (receipt.findings.includes('unsafe-provider-metadata')) {
+ return 'replace-private-billing-fields-before-delivery';
+ }
+
+ if (receipt.findings.includes('restricted-dataset-reference')) {
+ return 'replace-restricted-dataset-detail-with-usage-category';
+ }
+
+ if (
+ receipt.findings.includes('invalid-billing-amount') ||
+ receipt.findings.includes('invalid-billing-quantity') ||
+ receipt.findings.includes('malformed-line-item')
+ ) {
+ return 'repair-malformed-billing-fields-before-delivery';
+ }
+
+ return 'redact-private-research-context';
+}
+
+function evaluateReceiptPrivacy(batch) {
+ const receipts = evidenceList(batch.receipts).map((receipt, index) => evaluateReceipt(receipt, index));
+ const remediationActions = receipts
+ .filter((receipt) => receipt.decision === 'hold-for-finance-review')
+ .map((receipt) => ({
+ id: `remediate-${receipt.id}`,
+ receiptId: receipt.id,
+ action: remediationAction(receipt),
+ priority:
+ receipt.findings.includes('restricted-dataset-reference') ||
+ receipt.findings.includes('private-research-context') ||
+ receipt.findings.includes('unsafe-provider-metadata')
+ ? 'high'
+ : 'normal',
+ findings: receipt.findings
+ }));
+
+ const summary = {
+ deliverableReceipts: receipts.filter((receipt) => receipt.decision === 'deliver-receipt').length,
+ heldReceipts: receipts.filter((receipt) => receipt.decision === 'hold-for-finance-review').length,
+ remediationActions: remediationActions.length,
+ totalCentsReviewed: receipts.reduce((total, receipt) => {
+ const amount = receipt.customerCopy.totalCents;
+ return total + (Number.isFinite(amount) ? amount : 0);
+ }, 0)
+ };
+
+ return {
+ batchId: batch.batchId,
+ generatedAt: batch.generatedAt,
+ receipts,
+ remediationActions,
+ summary,
+ auditDigest: digest({
+ batchId: batch.batchId,
+ generatedAt: batch.generatedAt,
+ receipts,
+ remediationActions,
+ summary
+ })
+ };
+}
+
+function buildSampleBatch() {
+ return {
+ batchId: 'billing-privacy-review-20',
+ generatedAt: '2026-05-28T09:00:00Z',
+ receipts: [
+ {
+ id: 'receipt-safe-lab-plan',
+ invoiceId: 'inv-safe-lab-plan',
+ customerId: 'customer-lab-001',
+ currency: 'USD',
+ totalCents: 29900,
+ providerMetadata: {
+ accountRef: 'acct-lab-001',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-safe-lab-plan',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-lab-plan',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 29900
+ }
+ ]
+ },
+ {
+ id: 'receipt-private-compute',
+ invoiceId: 'inv-private-compute',
+ customerId: 'customer-lab-002',
+ currency: 'USD',
+ totalCents: 122500,
+ providerMetadata: {
+ accountRef: 'acct-lab-002',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-private-compute',
+ plan: 'institution-compute',
+ projectTitle: 'Alzheimer single-cell pilot',
+ collaboratorHandle: '@lab-private-reviewer',
+ 'GSE-private-cohort': 'restricted metadata field name'
+ },
+ lineItems: [
+ {
+ id: 'line-private-compute',
+ description: 'GPU inference for Alzheimer single-cell pilot',
+ usageCategory: 'ai-compute',
+ quantity: 250,
+ unit: 'compute-hour',
+ amountCents: 87500,
+ projectRef: 'irb-workspace-44'
+ },
+ {
+ id: 'line-private-dataset',
+ description: 'Storage for GSE-private controlled-access dataset',
+ usageCategory: 'storage',
+ quantity: 700,
+ unit: 'gb-month',
+ amountCents: 35000,
+ projectRef: 'restricted dataset locker'
+ }
+ ]
+ }
+ ]
+ };
+}
+
+module.exports = {
+ evaluateReceiptPrivacy,
+ buildSampleBatch,
+ digest
+};
diff --git a/billing-receipt-privacy-guard/make-demo-video.py b/billing-receipt-privacy-guard/make-demo-video.py
new file mode 100644
index 00000000..6ef6e56e
--- /dev/null
+++ b/billing-receipt-privacy-guard/make-demo-video.py
@@ -0,0 +1,64 @@
+import os
+import subprocess
+
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+REPORTS = os.path.join(HERE, "reports")
+FRAME = os.path.join(REPORTS, "demo-frame.png")
+OUTPUT = os.path.join(REPORTS, "demo.mp4")
+os.makedirs(REPORTS, exist_ok=True)
+
+
+def draw_frame_with_pillow():
+ from PIL import Image, ImageDraw, ImageFont
+
+ image = Image.new("RGB", (1280, 720), "#102027")
+ draw = ImageDraw.Draw(image)
+ draw.rounded_rectangle((54, 58, 1226, 662), radius=16, fill="#17313a", outline="#9bd67a", width=4)
+
+ try:
+ title_font = ImageFont.truetype("arial.ttf", 48)
+ body_font = ImageFont.truetype("arial.ttf", 30)
+ note_font = ImageFont.truetype("arial.ttf", 26)
+ except OSError:
+ title_font = ImageFont.load_default()
+ body_font = ImageFont.load_default()
+ note_font = ImageFont.load_default()
+
+ draw.text((96, 102), "Billing Receipt Privacy Guard", fill="white", font=title_font)
+ draw.text((96, 190), "Safe receipts keep only allowed provider metadata", fill="#dff5d5", font=body_font)
+ draw.text((96, 248), "Private project and dataset details are redacted", fill="#dff5d5", font=body_font)
+ draw.text((96, 306), "Unsafe receipts are held for finance review", fill="#dff5d5", font=body_font)
+ draw.text((96, 364), "Sparse provider batches produce deterministic empty evidence", fill="#dff5d5", font=body_font)
+ draw.text((96, 422), "Malformed line items become repair evidence, not runtime failures", fill="#dff5d5", font=body_font)
+ draw.text((96, 514), "Synthetic data only. No payment, customer, or workspace systems are called.", fill="#ffd37a", font=note_font)
+
+ image.save(FRAME)
+
+
+draw_frame_with_pillow()
+
+cmd = [
+ "ffmpeg",
+ "-y",
+ "-loop",
+ "1",
+ "-i",
+ FRAME,
+ "-t",
+ "4",
+ "-r",
+ "30",
+ "-c:v",
+ "libx264",
+ "-pix_fmt",
+ "yuv420p",
+ "-movflags",
+ "+faststart",
+ OUTPUT,
+]
+
+subprocess.run(cmd, check=True)
+if os.path.exists(FRAME):
+ os.remove(FRAME)
+print(f"Wrote {os.path.relpath(OUTPUT, HERE)}")
diff --git a/billing-receipt-privacy-guard/package.json b/billing-receipt-privacy-guard/package.json
new file mode 100644
index 00000000..0ecb8c92
--- /dev/null
+++ b/billing-receipt-privacy-guard/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "billing-receipt-privacy-guard",
+ "version": "1.0.0",
+ "description": "Dependency-free billing receipt privacy guard for SCIBASE revenue infrastructure.",
+ "main": "index.js",
+ "private": true,
+ "scripts": {
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "video": "python make-demo-video.py",
+ "check": "npm test && npm run demo && npm run video"
+ }
+}
diff --git a/billing-receipt-privacy-guard/reports/demo.mp4 b/billing-receipt-privacy-guard/reports/demo.mp4
new file mode 100644
index 00000000..1f3d5275
Binary files /dev/null and b/billing-receipt-privacy-guard/reports/demo.mp4 differ
diff --git a/billing-receipt-privacy-guard/reports/empty-receipt-privacy-packet.json b/billing-receipt-privacy-guard/reports/empty-receipt-privacy-packet.json
new file mode 100644
index 00000000..9ae469ca
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/empty-receipt-privacy-packet.json
@@ -0,0 +1,13 @@
+{
+ "batchId": "billing-empty-review-20",
+ "generatedAt": "2026-05-30T12:00:00Z",
+ "receipts": [],
+ "remediationActions": [],
+ "summary": {
+ "deliverableReceipts": 0,
+ "heldReceipts": 0,
+ "remediationActions": 0,
+ "totalCentsReviewed": 0
+ },
+ "auditDigest": "sha256:600daa8274aae6ef49d5ea13eebb7841666aad4046ee736fe944e692d15f238a"
+}
diff --git a/billing-receipt-privacy-guard/reports/malformed-line-item-privacy-packet.json b/billing-receipt-privacy-guard/reports/malformed-line-item-privacy-packet.json
new file mode 100644
index 00000000..fb3c2b53
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/malformed-line-item-privacy-packet.json
@@ -0,0 +1,67 @@
+{
+ "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",
+ "decision": "hold-for-finance-review",
+ "findings": [
+ "malformed-line-item"
+ ],
+ "removedMetadataKeys": [],
+ "providerMetadata": {
+ "accountRef": "acct-lab-014",
+ "billingPeriod": "2026-05",
+ "invoiceRef": "inv-malformed-line-item",
+ "plan": "lab-pro"
+ },
+ "redactedLineItems": [
+ {
+ "id": "line-malformed-1",
+ "description": "Malformed billing line item requires finance repair",
+ "usageCategory": "billing-line-repair",
+ "findings": [
+ "malformed-line-item"
+ ]
+ }
+ ],
+ "customerCopy": {
+ "receiptId": "receipt-malformed-line-item",
+ "customerId": "customer-lab-014",
+ "currency": "USD",
+ "totalCents": 19900,
+ "lineItems": [
+ {
+ "id": "line-malformed-1",
+ "description": "Malformed billing line item requires finance repair",
+ "usageCategory": "billing-line-repair",
+ "quantity": null,
+ "unit": "usage-unit",
+ "amountCents": null
+ }
+ ]
+ },
+ "auditDigest": "sha256:25a546ee2ea4a53284a23f2a42bd6495de0768139b8b411d2abc0405c8072fcb"
+ }
+ ],
+ "remediationActions": [
+ {
+ "id": "remediate-receipt-malformed-line-item",
+ "receiptId": "receipt-malformed-line-item",
+ "action": "repair-malformed-billing-fields-before-delivery",
+ "priority": "normal",
+ "findings": [
+ "malformed-line-item"
+ ]
+ }
+ ],
+ "summary": {
+ "deliverableReceipts": 0,
+ "heldReceipts": 1,
+ "remediationActions": 1,
+ "totalCentsReviewed": 19900
+ },
+ "auditDigest": "sha256:5237f198878acd22edd9df965ade3ed79888abb9e4f37ddd736c05cdf2ceb83d"
+}
diff --git a/billing-receipt-privacy-guard/reports/malformed-receipt-privacy-packet.json b/billing-receipt-privacy-guard/reports/malformed-receipt-privacy-packet.json
new file mode 100644
index 00000000..65801911
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/malformed-receipt-privacy-packet.json
@@ -0,0 +1,70 @@
+{
+ "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",
+ "decision": "hold-for-finance-review",
+ "findings": [
+ "invalid-billing-amount",
+ "invalid-billing-quantity"
+ ],
+ "removedMetadataKeys": [],
+ "providerMetadata": {
+ "accountRef": "acct-lab-013",
+ "billingPeriod": "2026-05",
+ "invoiceRef": "inv-malformed-numeric-fields",
+ "plan": "lab-pro"
+ },
+ "redactedLineItems": [
+ {
+ "id": "line-malformed-quantity",
+ "description": "Research platform service",
+ "usageCategory": "subscription",
+ "findings": [
+ "invalid-billing-amount",
+ "invalid-billing-quantity"
+ ]
+ }
+ ],
+ "customerCopy": {
+ "receiptId": "receipt-malformed-numeric-fields",
+ "customerId": "customer-lab-013",
+ "currency": "USD",
+ "totalCents": null,
+ "lineItems": [
+ {
+ "id": "line-malformed-quantity",
+ "description": "Research platform service",
+ "usageCategory": "subscription",
+ "quantity": null,
+ "unit": "month",
+ "amountCents": null
+ }
+ ]
+ },
+ "auditDigest": "sha256:4a5ec7c1505990fe77bed26177f50c19d29b2cd8179bae3f19c95478af361100"
+ }
+ ],
+ "remediationActions": [
+ {
+ "id": "remediate-receipt-malformed-numeric-fields",
+ "receiptId": "receipt-malformed-numeric-fields",
+ "action": "repair-malformed-billing-fields-before-delivery",
+ "priority": "normal",
+ "findings": [
+ "invalid-billing-amount",
+ "invalid-billing-quantity"
+ ]
+ }
+ ],
+ "summary": {
+ "deliverableReceipts": 0,
+ "heldReceipts": 1,
+ "remediationActions": 1,
+ "totalCentsReviewed": 0
+ },
+ "auditDigest": "sha256:b2b35b8d83e1bf7fab9388e8086eb7f2cc5fc5ad226bb2b9e80203b5e2087066"
+}
diff --git a/billing-receipt-privacy-guard/reports/receipt-privacy-packet.json b/billing-receipt-privacy-guard/reports/receipt-privacy-packet.json
new file mode 100644
index 00000000..397ccaa5
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/receipt-privacy-packet.json
@@ -0,0 +1,132 @@
+{
+ "batchId": "billing-privacy-review-20",
+ "generatedAt": "2026-05-28T09:00:00Z",
+ "receipts": [
+ {
+ "id": "receipt-safe-lab-plan",
+ "invoiceId": "inv-safe-lab-plan",
+ "customerId": "customer-lab-001",
+ "decision": "deliver-receipt",
+ "findings": [],
+ "removedMetadataKeys": [],
+ "providerMetadata": {
+ "accountRef": "acct-lab-001",
+ "billingPeriod": "2026-05",
+ "invoiceRef": "inv-safe-lab-plan",
+ "plan": "lab-pro"
+ },
+ "redactedLineItems": [
+ {
+ "id": "line-lab-plan",
+ "description": "Lab Pro monthly subscription",
+ "usageCategory": "subscription",
+ "findings": []
+ }
+ ],
+ "customerCopy": {
+ "receiptId": "receipt-safe-lab-plan",
+ "customerId": "customer-lab-001",
+ "currency": "USD",
+ "totalCents": 29900,
+ "lineItems": [
+ {
+ "id": "line-lab-plan",
+ "description": "Lab Pro monthly subscription",
+ "usageCategory": "subscription",
+ "quantity": 1,
+ "unit": "month",
+ "amountCents": 29900
+ }
+ ]
+ },
+ "auditDigest": "sha256:920e09e8abf433174de17eca8ca6b7e437f397637f333da492ac02d09c226c29"
+ },
+ {
+ "id": "receipt-private-compute",
+ "invoiceId": "inv-private-compute",
+ "customerId": "customer-lab-002",
+ "decision": "hold-for-finance-review",
+ "findings": [
+ "collaborator-identifier",
+ "private-research-context",
+ "restricted-dataset-reference",
+ "unsafe-provider-metadata"
+ ],
+ "removedMetadataKeys": [
+ "metadata-key-redacted-1",
+ "metadata-key-redacted-2",
+ "projectTitle"
+ ],
+ "providerMetadata": {
+ "accountRef": "acct-lab-002",
+ "billingPeriod": "2026-05",
+ "invoiceRef": "inv-private-compute",
+ "plan": "institution-compute"
+ },
+ "redactedLineItems": [
+ {
+ "id": "line-private-compute",
+ "description": "AI compute usage for restricted research workspace",
+ "usageCategory": "ai-compute",
+ "findings": [
+ "private-research-context"
+ ]
+ },
+ {
+ "id": "line-private-dataset",
+ "description": "Restricted dataset storage and processing",
+ "usageCategory": "storage",
+ "findings": [
+ "restricted-dataset-reference"
+ ]
+ }
+ ],
+ "customerCopy": {
+ "receiptId": "receipt-private-compute",
+ "customerId": "customer-lab-002",
+ "currency": "USD",
+ "totalCents": 122500,
+ "lineItems": [
+ {
+ "id": "line-private-compute",
+ "description": "AI compute usage for restricted research workspace",
+ "usageCategory": "ai-compute",
+ "quantity": 250,
+ "unit": "compute-hour",
+ "amountCents": 87500
+ },
+ {
+ "id": "line-private-dataset",
+ "description": "Restricted dataset storage and processing",
+ "usageCategory": "storage",
+ "quantity": 700,
+ "unit": "gb-month",
+ "amountCents": 35000
+ }
+ ]
+ },
+ "auditDigest": "sha256:7e05da13f9d6502443aee8363bb9290c27de9dc06484ff36c66300a8534fefdc"
+ }
+ ],
+ "remediationActions": [
+ {
+ "id": "remediate-receipt-private-compute",
+ "receiptId": "receipt-private-compute",
+ "action": "replace-private-billing-fields-before-delivery",
+ "priority": "high",
+ "findings": [
+ "collaborator-identifier",
+ "private-research-context",
+ "restricted-dataset-reference",
+ "unsafe-provider-metadata"
+ ]
+ }
+ ],
+ "summary": {
+ "deliverableReceipts": 1,
+ "heldReceipts": 1,
+ "remediationActions": 1,
+ "totalCentsReviewed": 152400
+ },
+ "auditDigest": "sha256:08e21ce3fc6915ed223f64220fdcc534986805530e3a1169f3d73ebe8860930f"
+}
diff --git a/billing-receipt-privacy-guard/reports/receipt-privacy-report.md b/billing-receipt-privacy-guard/reports/receipt-privacy-report.md
new file mode 100644
index 00000000..5ad92734
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/receipt-privacy-report.md
@@ -0,0 +1,37 @@
+# Billing Receipt Privacy Guard
+
+Batch: billing-privacy-review-20
+Generated: 2026-05-28T09:00:00Z
+
+## Summary
+
+- Deliverable receipts: 1
+- Held receipts: 1
+- Remediation actions: 1
+- Total cents reviewed: 152400
+- Audit digest: sha256:08e21ce3fc6915ed223f64220fdcc534986805530e3a1169f3d73ebe8860930f
+
+## Receipt Decisions
+
+- receipt-safe-lab-plan: deliver-receipt, findings: none
+- receipt-private-compute: hold-for-finance-review, findings: collaborator-identifier, private-research-context, restricted-dataset-reference, unsafe-provider-metadata
+
+## Remediation Actions
+
+- remediate-receipt-private-compute: replace-private-billing-fields-before-delivery (high)
+
+## 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 0 receipts and generated 0 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 hold-for-finance-review, and customer-facing numeric fields are redacted to null.
+
+## Malformed Line Item Guard
+
+Malformed line-item entries are held before delivery instead of crashing receipt review. The malformed line-item fixture decision is hold-for-finance-review, and the customer-facing line item id is line-malformed-1.
+
+## Safety
+
+All fixtures are synthetic. The guard does not call payment processors, customer systems, private workspaces, institutional finance tools, or external APIs.
diff --git a/billing-receipt-privacy-guard/reports/summary.svg b/billing-receipt-privacy-guard/reports/summary.svg
new file mode 100644
index 00000000..38aa50c0
--- /dev/null
+++ b/billing-receipt-privacy-guard/reports/summary.svg
@@ -0,0 +1,11 @@
+
diff --git a/billing-receipt-privacy-guard/requirements-map.md b/billing-receipt-privacy-guard/requirements-map.md
new file mode 100644
index 00000000..27cc5f23
--- /dev/null
+++ b/billing-receipt-privacy-guard/requirements-map.md
@@ -0,0 +1,35 @@
+# Requirements Map
+
+## Tiered Subscription Billing
+
+- Keeps safe subscription receipts deliverable with a provider-metadata allowlist.
+- Removes project titles, collaborator handles, and private research descriptors from receipt metadata.
+- Scans nested provider metadata values so allowlisted keys cannot hide private workspace context.
+- Redacts unsafe provider metadata key names when the key itself carries restricted dataset or private research context.
+- Treats omitted provider metadata as an empty provider packet instead of crashing receipt review.
+- Redacts receipt, invoice, and customer identifiers when they carry private project, dataset, or collaborator context.
+- Redacts unsafe customer-facing currency labels when they carry restricted dataset context.
+- Redacts unsafe customer-facing totals, quantities, and line-item amounts when they carry restricted dataset context.
+- Holds receipts and redacts customer-facing totals, quantities, and line-item amounts when numeric billing fields are malformed or negative.
+- Redacts customer-facing line-item identifiers and units when they carry restricted dataset context.
+- Holds malformed line-item entries with finance-repair evidence instead of crashing receipt review.
+- Treats omitted receipt and line-item collections as empty billing evidence instead of crashing receipt review.
+- Preserves customer-useful totals, billing period, plan, and invoice references after redaction.
+
+## AI Compute Billing
+
+- Detects private compute line items that expose restricted research project context.
+- Replaces specific project descriptions with usage-category-safe customer copy.
+- Holds unsafe receipts before external delivery when compute or storage lines contain restricted details.
+
+## Licensing APIs And Analytics
+
+- Treats analytics licensing as a billable service category without exposing private corpus details.
+- Produces deterministic audit packets for finance and compliance review.
+- Keeps receipt evidence synthetic and independent of live customer or payment systems.
+
+## Safety And Scope
+
+- Synthetic data only.
+- No credentials, payment processor calls, customer systems, private workspaces, institutional finance tools, or external APIs.
+- This slice is distinct from pricing, tax, disputes, payment rails, webhook entitlement, invoice acceptance, procurement, subscription renewal, usage reconciliation, storage overage, and analytics licensing gates.
diff --git a/billing-receipt-privacy-guard/test.js b/billing-receipt-privacy-guard/test.js
new file mode 100644
index 00000000..03e99308
--- /dev/null
+++ b/billing-receipt-privacy-guard/test.js
@@ -0,0 +1,552 @@
+const assert = require('assert');
+const {
+ evaluateReceiptPrivacy,
+ buildSampleBatch
+} = require('./index');
+
+function byId(items, id) {
+ return items.find((item) => item.id === id);
+}
+
+function testSafeReceiptIsDeliverableWithOnlyAllowedMetadata() {
+ const result = evaluateReceiptPrivacy(buildSampleBatch());
+ const receipt = byId(result.receipts, 'receipt-safe-lab-plan');
+
+ assert.equal(receipt.decision, 'deliver-receipt');
+ assert.equal(receipt.findings.length, 0);
+ assert.deepEqual(receipt.providerMetadata, {
+ accountRef: 'acct-lab-001',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-safe-lab-plan',
+ plan: 'lab-pro'
+ });
+}
+
+function testPrivateResearchContextIsRedactedBeforeReceiptDelivery() {
+ const result = evaluateReceiptPrivacy(buildSampleBatch());
+ const receipt = byId(result.receipts, 'receipt-private-compute');
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('private-research-context'), true);
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(receipt.redactedLineItems[0].description, 'AI compute usage for restricted research workspace');
+ assert.equal(receipt.redactedLineItems[1].description, 'Restricted dataset storage and processing');
+}
+
+function testProviderMetadataAllowlistBlocksOverSpecificFields() {
+ const result = evaluateReceiptPrivacy(buildSampleBatch());
+ const receipt = byId(result.receipts, 'receipt-private-compute');
+
+ assert.equal(receipt.findings.includes('unsafe-provider-metadata'), true);
+ assert.equal(Object.prototype.hasOwnProperty.call(receipt.providerMetadata, 'projectTitle'), false);
+ assert.equal(Object.prototype.hasOwnProperty.call(receipt.providerMetadata, 'collaboratorHandle'), false);
+
+ const action = byId(result.remediationActions, 'remediate-receipt-private-compute');
+ assert.equal(action.action, 'replace-private-billing-fields-before-delivery');
+ assert.equal(action.priority, 'high');
+}
+
+function testNestedAllowedMetadataStillScansPrivateContext() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-nested-metadata',
+ invoiceId: 'inv-nested-metadata',
+ customerId: 'customer-lab-003',
+ currency: 'USD',
+ totalCents: 45000,
+ providerMetadata: {
+ accountRef: {
+ workspace: 'IRB Alzheimer single-cell workspace',
+ billingId: 'acct-lab-003'
+ },
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-nested-metadata',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-safe-subscription',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 45000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = byId(result.receipts, 'receipt-nested-metadata');
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('private-research-context'), true);
+ assert.equal(Object.prototype.hasOwnProperty.call(receipt.providerMetadata, 'accountRef'), false);
+}
+
+function testUnsafeProviderMetadataKeyNamesAreRedacted() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-metadata-key-leak',
+ invoiceId: 'inv-metadata-key-leak',
+ customerId: 'customer-lab-011',
+ currency: 'USD',
+ totalCents: 18000,
+ providerMetadata: {
+ accountRef: 'acct-lab-011',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-metadata-key-leak',
+ plan: 'lab-pro',
+ 'GSE-private-cohort': 'metadata field name carries restricted context'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription-f',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 18000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(receipt.removedMetadataKeys.includes('metadata-key-redacted-1'), true);
+ assert.equal(JSON.stringify(result).includes('GSE-private'), false);
+}
+
+function testCustomerFacingLineItemFieldsAreRedacted() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-line-field-leak',
+ invoiceId: 'inv-line-field-leak',
+ customerId: 'customer-lab-004',
+ currency: 'USD',
+ totalCents: 28000,
+ providerMetadata: {
+ accountRef: 'acct-lab-004',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-line-field-leak',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-GSE-private-cohort-storage',
+ description: 'Storage usage',
+ usageCategory: 'storage',
+ quantity: 80,
+ unit: 'GSE-private gb-month',
+ amountCents: 28000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = byId(result.receipts, 'receipt-line-field-leak');
+ const lineItem = receipt.customerCopy.lineItems[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(lineItem.id, 'line-redacted-1');
+ assert.equal(lineItem.unit, 'usage-unit');
+ assert.equal(lineItem.description, 'Restricted dataset storage and processing');
+ assert.equal(JSON.stringify(receipt).includes('GSE-private'), false);
+}
+
+function testCustomerFacingReceiptIdentifiersAreRedacted() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-alzheimer-trial-001',
+ invoiceId: 'inv-GSE-private-collaborator',
+ customerId: 'customer-@private-reviewer',
+ currency: 'USD',
+ totalCents: 49000,
+ providerMetadata: {
+ accountRef: 'acct-lab-005',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-public-safe',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 49000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('private-research-context'), true);
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(receipt.findings.includes('collaborator-identifier'), true);
+ assert.equal(receipt.id, 'receipt-redacted-1');
+ assert.equal(receipt.invoiceId, 'invoice-redacted-1');
+ assert.equal(receipt.customerId, 'customer-redacted-1');
+ assert.equal(receipt.customerCopy.receiptId, 'receipt-redacted-1');
+ assert.equal(receipt.customerCopy.customerId, 'customer-redacted-1');
+ assert.equal(JSON.stringify(result).includes('alzheimer'), false);
+ assert.equal(JSON.stringify(result).includes('GSE-private'), false);
+ assert.equal(JSON.stringify(result).includes('@private-reviewer'), false);
+}
+
+function testRedactedReceiptIdentifiersRemainDistinct() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-alzheimer-trial-001',
+ invoiceId: 'inv-alzheimer-trial-001',
+ customerId: 'customer-lab-006',
+ currency: 'USD',
+ totalCents: 12000,
+ providerMetadata: {
+ accountRef: 'acct-lab-006',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-public-safe-006',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription-a',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 12000
+ }
+ ]
+ },
+ {
+ id: 'receipt-alzheimer-trial-002',
+ invoiceId: 'inv-alzheimer-trial-002',
+ customerId: 'customer-lab-007',
+ currency: 'USD',
+ totalCents: 13000,
+ providerMetadata: {
+ accountRef: 'acct-lab-007',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-public-safe-007',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription-b',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 13000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+
+ assert.deepEqual(result.receipts.map((receipt) => receipt.id), [
+ 'receipt-redacted-1',
+ 'receipt-redacted-2'
+ ]);
+ assert.equal(new Set(result.receipts.map((receipt) => receipt.invoiceId)).size, 2);
+}
+
+function testMissingProviderMetadataIsTreatedAsEmptyMetadata() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-missing-provider-metadata',
+ invoiceId: 'inv-missing-provider-metadata',
+ customerId: 'customer-lab-008',
+ currency: 'USD',
+ totalCents: 31000,
+ lineItems: [
+ {
+ id: 'line-platform-subscription-c',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 31000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+
+ assert.equal(receipt.decision, 'deliver-receipt');
+ assert.deepEqual(receipt.providerMetadata, {});
+ assert.deepEqual(receipt.removedMetadataKeys, []);
+ assert.equal(receipt.findings.length, 0);
+}
+
+function testMissingReceiptListProducesEmptyReviewPacket() {
+ const batch = {
+ batchId: 'billing-empty-review-20',
+ generatedAt: '2026-05-30T12:00:00Z'
+ };
+
+ const result = evaluateReceiptPrivacy(batch);
+
+ assert.deepEqual(result.receipts, []);
+ assert.deepEqual(result.remediationActions, []);
+ assert.equal(result.summary.deliverableReceipts, 0);
+ assert.equal(result.summary.heldReceipts, 0);
+ assert.equal(result.summary.remediationActions, 0);
+ assert.equal(result.summary.totalCentsReviewed, 0);
+ assert.ok(result.auditDigest.startsWith('sha256:'));
+}
+
+function testMissingLineItemsAreTreatedAsEmptyReceiptLines() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-missing-line-items',
+ invoiceId: 'inv-missing-line-items',
+ customerId: 'customer-lab-012',
+ currency: 'USD',
+ totalCents: 9900,
+ providerMetadata: {
+ accountRef: 'acct-lab-012',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-missing-line-items',
+ plan: 'lab-pro'
+ }
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+
+ assert.equal(receipt.decision, 'deliver-receipt');
+ assert.deepEqual(receipt.customerCopy.lineItems, []);
+ assert.deepEqual(receipt.redactedLineItems, []);
+ assert.equal(result.summary.deliverableReceipts, 1);
+ assert.equal(result.summary.heldReceipts, 0);
+ assert.equal(result.summary.totalCentsReviewed, 9900);
+}
+
+function testCustomerFacingCurrencyLabelsAreRedacted() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-currency-leak',
+ invoiceId: 'inv-currency-leak',
+ customerId: 'customer-lab-009',
+ currency: 'USD GSE-private cohort',
+ totalCents: 27000,
+ providerMetadata: {
+ accountRef: 'acct-lab-009',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-currency-leak',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription-d',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: 1,
+ unit: 'month',
+ amountCents: 27000
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(receipt.customerCopy.currency, 'XXX');
+ assert.equal(JSON.stringify(result).includes('GSE-private'), false);
+}
+
+function testCustomerFacingMoneyAndQuantityFieldsAreRedacted() {
+ const batch = buildSampleBatch();
+ batch.receipts = [
+ {
+ id: 'receipt-amount-leak',
+ invoiceId: 'inv-amount-leak',
+ customerId: 'customer-lab-010',
+ currency: 'USD',
+ totalCents: '27000 GSE-private cohort',
+ providerMetadata: {
+ accountRef: 'acct-lab-010',
+ billingPeriod: '2026-05',
+ invoiceRef: 'inv-amount-leak',
+ plan: 'lab-pro'
+ },
+ lineItems: [
+ {
+ id: 'line-platform-subscription-e',
+ description: 'Lab Pro monthly subscription',
+ usageCategory: 'subscription',
+ quantity: '1 participant from GSE-private cohort',
+ unit: 'month',
+ amountCents: '27000 GSE-private cohort'
+ }
+ ]
+ }
+ ];
+
+ const result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+ const lineItem = receipt.customerCopy.lineItems[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('restricted-dataset-reference'), true);
+ assert.equal(receipt.customerCopy.totalCents, null);
+ assert.equal(lineItem.quantity, null);
+ assert.equal(lineItem.amountCents, null);
+ assert.equal(result.summary.totalCentsReviewed, 0);
+ assert.equal(JSON.stringify(result).includes('GSE-private'), false);
+}
+
+function testMalformedCustomerFacingMoneyAndQuantityFieldsAreHeld() {
+ const batch = buildSampleBatch();
+ batch.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 result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+ const lineItem = receipt.customerCopy.lineItems[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('invalid-billing-amount'), true);
+ assert.equal(receipt.findings.includes('invalid-billing-quantity'), true);
+ assert.equal(receipt.customerCopy.totalCents, null);
+ assert.equal(lineItem.quantity, null);
+ assert.equal(lineItem.amountCents, null);
+ assert.equal(result.summary.totalCentsReviewed, 0);
+
+ const action = result.remediationActions[0];
+ assert.equal(action.action, 'repair-malformed-billing-fields-before-delivery');
+ assert.equal(action.priority, 'normal');
+}
+
+function testMalformedLineItemEntriesAreHeldInsteadOfCrashing() {
+ const batch = buildSampleBatch();
+ batch.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 result = evaluateReceiptPrivacy(batch);
+ const receipt = result.receipts[0];
+ const lineItem = receipt.customerCopy.lineItems[0];
+
+ assert.equal(receipt.decision, 'hold-for-finance-review');
+ assert.equal(receipt.findings.includes('malformed-line-item'), true);
+ assert.equal(lineItem.id, 'line-malformed-1');
+ assert.equal(lineItem.quantity, null);
+ assert.equal(lineItem.amountCents, null);
+
+ const action = result.remediationActions[0];
+ assert.equal(action.action, 'repair-malformed-billing-fields-before-delivery');
+ assert.equal(action.priority, 'normal');
+}
+
+function testCustomerCopyRemainsUsefulAfterRedaction() {
+ const result = evaluateReceiptPrivacy(buildSampleBatch());
+ const receipt = byId(result.receipts, 'receipt-private-compute');
+
+ assert.equal(receipt.customerCopy.totalCents, 122500);
+ assert.equal(receipt.customerCopy.currency, 'USD');
+ assert.equal(receipt.customerCopy.lineItems.length, 2);
+ assert.equal(receipt.customerCopy.lineItems[0].usageCategory, 'ai-compute');
+ assert.equal(receipt.customerCopy.lineItems[1].usageCategory, 'storage');
+}
+
+function testAuditDigestIsDeterministicAndPrivateFree() {
+ const first = evaluateReceiptPrivacy(buildSampleBatch());
+ const second = evaluateReceiptPrivacy(buildSampleBatch());
+
+ assert.equal(first.auditDigest, second.auditDigest);
+ assert.ok(first.auditDigest.startsWith('sha256:'));
+ assert.equal(first.summary.deliverableReceipts, 1);
+ assert.equal(first.summary.heldReceipts, 1);
+ assert.equal(JSON.stringify(first).includes('Alzheimer'), false);
+ assert.equal(JSON.stringify(first).includes('GSE-private'), false);
+}
+
+const tests = [
+ testSafeReceiptIsDeliverableWithOnlyAllowedMetadata,
+ testPrivateResearchContextIsRedactedBeforeReceiptDelivery,
+ testProviderMetadataAllowlistBlocksOverSpecificFields,
+ testNestedAllowedMetadataStillScansPrivateContext,
+ testUnsafeProviderMetadataKeyNamesAreRedacted,
+ testCustomerFacingLineItemFieldsAreRedacted,
+ testCustomerFacingReceiptIdentifiersAreRedacted,
+ testRedactedReceiptIdentifiersRemainDistinct,
+ testMissingProviderMetadataIsTreatedAsEmptyMetadata,
+ testMissingReceiptListProducesEmptyReviewPacket,
+ testMissingLineItemsAreTreatedAsEmptyReceiptLines,
+ testCustomerFacingCurrencyLabelsAreRedacted,
+ testCustomerFacingMoneyAndQuantityFieldsAreRedacted,
+ testMalformedCustomerFacingMoneyAndQuantityFieldsAreHeld,
+ testMalformedLineItemEntriesAreHeldInsteadOfCrashing,
+ testCustomerCopyRemainsUsefulAfterRedaction,
+ testAuditDigestIsDeterministicAndPrivateFree
+];
+
+for (const test of tests) {
+ test();
+}
+
+console.log(`${tests.length} billing receipt privacy guard tests passed`);