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
38 changes: 38 additions & 0 deletions payment-webhook-entitlement-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Payment Webhook Entitlement Guard

This module is a focused revenue-infrastructure slice for issue #20. It protects the exact activation point where paid orders, AI compute top-ups, lab seats, and institutional subscriptions become usable after payment webhooks arrive.

It is intentionally not another generic billing ledger. The guard checks webhook streams before granting entitlements:

- duplicate provider event IDs
- invalid or stale signatures
- test-mode payment events
- missing order references
- amount or currency mismatches
- out-of-order lifecycle events
- duplicate entitlement grants
- refund, dispute, or payment reversal events

The output is a deterministic finance packet with blockers, warnings, activation candidates, reversal actions, and a release recommendation.

## Run

```bash
node payment-webhook-entitlement-guard/test.js
node payment-webhook-entitlement-guard/demo.js
```

The demo writes:

- `reports/webhook-entitlement-packet.json`
- `reports/webhook-entitlement-report.md`
- `reports/summary.svg`
- `reports/summary.png`
- `reports/demo.mp4`

## Design Notes

- Dependency-free Node.js core logic.
- Synthetic data only. No live payment credentials.
- Supports Stripe, PayPal, and institutional invoice event streams.
- Entitlements are withheld whenever blockers exist.
25 changes: 25 additions & 0 deletions payment-webhook-entitlement-guard/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Acceptance Notes

## What To Review

- `index.js` implements the webhook-to-entitlement activation guard.
- `sample-data.js` includes risky duplicate, mismatched, invalid, stale, and reversal events plus a clean activation case.
- `test.js` validates duplicate prevention, amount mismatch, reversal holds, clean activation, and report rendering.
- `demo.js` generates the finance packet and visual demo artifact.

## Verification

```bash
node payment-webhook-entitlement-guard/test.js
node payment-webhook-entitlement-guard/demo.js
node --check payment-webhook-entitlement-guard/index.js
node --check payment-webhook-entitlement-guard/sample-data.js
node --check payment-webhook-entitlement-guard/test.js
node --check payment-webhook-entitlement-guard/demo.js
git diff --check
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,duration -of default=noprint_wrappers=1 payment-webhook-entitlement-guard/reports/demo.mp4
```

## Expected Demo Result

The risky packet returns `hold_entitlement_activation` with blockers for duplicate webhook replay, existing grant double-credit risk, amount mismatch, invalid/stale signature, and refund/reversal reconciliation. The clean packet returns `activate_paid_entitlements`.
51 changes: 51 additions & 0 deletions payment-webhook-entitlement-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const { spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const { evaluatePaymentWebhookActivation, writeReportBundle } = require("./index");
const { riskyPacket } = require("./sample-data");

const reportsDir = path.join(__dirname, "reports");
const result = evaluatePaymentWebhookActivation(riskyPacket);
const paths = writeReportBundle(result, reportsDir);
const pngPath = path.join(reportsDir, "summary.png");
const mp4Path = path.join(reportsDir, "demo.mp4");

function runCommand(command, args) {
const child = spawnSync(command, args, { encoding: "utf8" });
if (child.status !== 0) {
throw new Error(`${command} failed: ${child.stderr || child.stdout}`);
}
}

if (fs.existsSync(paths.svgPath)) {
runCommand("rsvg-convert", ["-w", "1280", "-h", "720", paths.svgPath, "-o", pngPath]);
runCommand("ffmpeg", [
"-y",
"-loop",
"1",
"-i",
pngPath,
"-t",
"12",
"-vf",
"format=yuv420p",
"-c:v",
"libx264",
"-movflags",
"+faststart",
mp4Path
]);
}

console.log(
[
`status=${result.status}`,
`blockers=${result.summary.blockers}`,
`warnings=${result.summary.warnings}`,
`activationCandidates=${result.summary.activationCandidates}`,
`reversals=${result.summary.reversals}`,
`report=${paths.markdownPath}`,
`packet=${paths.jsonPath}`,
`demo=${mp4Path}`
].join("\n")
);
Loading