@portalshq/monetization 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@portalshq/monetization",
3
+ "version": "0.2.0",
4
+ "description": "Audience-to-creator monetization for Portals: Stripe Connect destination charges, entitlements, ledger, and transactional outbox. Keyed on tenant, not channel.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/portalshq/portals-cloud.git",
8
+ "directory": "packages/monetization"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "sql",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "rm -rf dist && tsc -p tsconfig.json",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "test": "vitest run"
28
+ },
29
+ "dependencies": {
30
+ "pg": "^8.16.3",
31
+ "stripe": "^22.4.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.0.0",
35
+ "@types/pg": "^8.15.5",
36
+ "typescript": "^5.5.0",
37
+ "vitest": "^3.2.7"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ }
43
+ }
@@ -0,0 +1,158 @@
1
+ -- @portalshq/monetization schema.
2
+ --
3
+ -- Connected accounts are created via Accounts v2 as a **recipient**, for
4
+ -- destination charges. The end user's Customer and payment method live on the
5
+ -- PLATFORM account, so one payment method is reused across every tenant they
6
+ -- pay; funds transfer to the tenant on success.
7
+ --
8
+ -- `losses_collector: "application"` is required for destination charges: the
9
+ -- platform must hold the negative balance so a dispute transfer can be
10
+ -- reversed. The accepted cost is that the platform is merchant of record and
11
+ -- carries dispute liability across tenant volume.
12
+ --
13
+ -- This file also replaces the pre-0.2.0 `billing_*` tables, which keyed
14
+ -- profiles on channel. No production data was written against those tables, so
15
+ -- the change is a clean replacement rather than a migration.
16
+
17
+ CREATE TABLE IF NOT EXISTS monetization_tenant_profiles (
18
+ tenant_id text PRIMARY KEY,
19
+ owner_id text NOT NULL,
20
+ -- The tenant's own connected account, from `stripe.v2.core.accounts.create`.
21
+ stripe_account_id text NOT NULL,
22
+ -- v2 capability path:
23
+ -- configuration.recipient.capabilities.stripe_balance.stripe_transfers.status.
24
+ -- The deprecated v1 booleans (charges_enabled, payouts_enabled) are not stored.
25
+ transfers_status text NOT NULL DEFAULT 'pending'
26
+ CHECK (transfers_status IN ('active', 'inactive', 'pending')),
27
+ default_currency text NOT NULL,
28
+ country text,
29
+ created_at timestamptz NOT NULL,
30
+ updated_at timestamptz NOT NULL
31
+ );
32
+ -- Capability changes arrive via account.updated, which carries the account id.
33
+ CREATE INDEX IF NOT EXISTS monetization_tenant_profiles_account_idx
34
+ ON monetization_tenant_profiles(stripe_account_id);
35
+
36
+ -- The platform holds the end-user Customer, so one saved payment method pays
37
+ -- for purchases across every tenant this consumer buys from. Keyed by buyer id
38
+ -- rather than tenant: the customer belongs to the consumer, not to a tenant.
39
+ CREATE TABLE IF NOT EXISTS monetization_billing_customers (
40
+ buyer_id text PRIMARY KEY,
41
+ stripe_customer_id text NOT NULL UNIQUE,
42
+ created_at timestamptz NOT NULL
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS monetization_purchases (
46
+ id text PRIMARY KEY,
47
+ tenant_id text NOT NULL REFERENCES monetization_tenant_profiles(tenant_id),
48
+ -- Attribution only. A tenant may own many channels and is paid once.
49
+ channel_id text NOT NULL,
50
+ -- Fact for session-scoped entitlement rules.
51
+ session_id text,
52
+ buyer_id text NOT NULL,
53
+ -- The platform-account Customer holding this consumer's saved payment method.
54
+ customer_id text,
55
+ product_key text NOT NULL,
56
+ kind text NOT NULL,
57
+ amount bigint NOT NULL CHECK (amount > 0),
58
+ currency text NOT NULL CHECK (currency ~ '^[a-z]{3}$'),
59
+ platform_fee_amount bigint NOT NULL CHECK (platform_fee_amount >= 0 AND platform_fee_amount <= amount),
60
+ state text NOT NULL,
61
+ stripe_checkout_session_id text UNIQUE,
62
+ stripe_payment_intent_id text UNIQUE,
63
+ stripe_charge_id text UNIQUE,
64
+ -- The Connect transfer to the tenant; reversed on dispute.
65
+ stripe_transfer_id text,
66
+ stripe_application_fee_id text,
67
+ refunded_amount bigint NOT NULL DEFAULT 0 CHECK (refunded_amount >= 0 AND refunded_amount <= amount),
68
+ settled_at timestamptz,
69
+ created_at timestamptz NOT NULL,
70
+ updated_at timestamptz NOT NULL
71
+ );
72
+ -- Tenant-level rollup across a tenant's applications.
73
+ CREATE INDEX IF NOT EXISTS monetization_purchases_tenant_idx
74
+ ON monetization_purchases(tenant_id, created_at);
75
+ -- Per-application rollup within a tenant.
76
+ CREATE INDEX IF NOT EXISTS monetization_purchases_channel_idx
77
+ ON monetization_purchases(channel_id, created_at);
78
+
79
+ CREATE TABLE IF NOT EXISTS monetization_stripe_events (
80
+ id text PRIMARY KEY,
81
+ type text NOT NULL,
82
+ created_at timestamptz NOT NULL,
83
+ processed_at timestamptz
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS monetization_ledger (
87
+ id text PRIMARY KEY,
88
+ purchase_id text NOT NULL REFERENCES monetization_purchases(id),
89
+ stripe_event_id text REFERENCES monetization_stripe_events(id),
90
+ kind text NOT NULL,
91
+ amount bigint NOT NULL,
92
+ currency text NOT NULL,
93
+ stripe_fee bigint,
94
+ net bigint,
95
+ metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
96
+ created_at timestamptz NOT NULL,
97
+ -- Makes double-application of a Stripe event impossible at the database level.
98
+ UNIQUE (stripe_event_id, kind, purchase_id)
99
+ );
100
+
101
+ CREATE TABLE IF NOT EXISTS monetization_outbox (
102
+ id text PRIMARY KEY,
103
+ type text NOT NULL,
104
+ aggregate_id text NOT NULL,
105
+ payload jsonb NOT NULL,
106
+ attempts integer NOT NULL DEFAULT 0,
107
+ claimed_at timestamptz,
108
+ delivered_at timestamptz,
109
+ -- Set when the event exceeded its attempt budget. The row is retained, never
110
+ -- deleted: a customer who paid still has a settlement event on record, and
111
+ -- requeueDeadOutbox can replay it once the cause is fixed.
112
+ dead_at timestamptz,
113
+ last_error text,
114
+ created_at timestamptz NOT NULL
115
+ );
116
+ -- Pending work only. Dead events are excluded so they cannot be re-claimed.
117
+ CREATE INDEX IF NOT EXISTS monetization_outbox_pending_idx
118
+ ON monetization_outbox(created_at) WHERE delivered_at IS NULL AND dead_at IS NULL;
119
+ -- Operator view of what needs attention.
120
+ CREATE INDEX IF NOT EXISTS monetization_outbox_dead_idx
121
+ ON monetization_outbox(created_at) WHERE dead_at IS NOT NULL;
122
+
123
+ -- Tenant-configured entitlement rules. `scope` holds the dimensions the tenant
124
+ -- named; the dimensions present are both the matcher and the grant's lifetime.
125
+ CREATE TABLE IF NOT EXISTS monetization_entitlement_rules (
126
+ id text PRIMARY KEY,
127
+ tenant_id text NOT NULL,
128
+ kind text NOT NULL,
129
+ scope jsonb NOT NULL,
130
+ conditions jsonb NOT NULL DEFAULT '[]'::jsonb,
131
+ quantity integer NOT NULL CHECK (quantity >= 1),
132
+ created_at timestamptz NOT NULL
133
+ );
134
+ CREATE INDEX IF NOT EXISTS monetization_entitlement_rules_tenant_idx
135
+ ON monetization_entitlement_rules(tenant_id);
136
+
137
+ -- Balances, not flags. A rule nothing consumes stays permanently available.
138
+ CREATE TABLE IF NOT EXISTS monetization_entitlements (
139
+ id text PRIMARY KEY,
140
+ rule_id text NOT NULL REFERENCES monetization_entitlement_rules(id),
141
+ tenant_id text NOT NULL,
142
+ -- The end user who paid. Distinct from tenant_id, which receives the money.
143
+ consumer_id text NOT NULL,
144
+ kind text NOT NULL,
145
+ scope jsonb NOT NULL,
146
+ quantity integer NOT NULL CHECK (quantity >= 1),
147
+ remaining integer NOT NULL CHECK (remaining >= 0 AND remaining <= quantity),
148
+ purchase_id text NOT NULL REFERENCES monetization_purchases(id),
149
+ expires_at timestamptz,
150
+ created_at timestamptz NOT NULL,
151
+ updated_at timestamptz NOT NULL,
152
+ -- One grant per rule per purchase: a redelivered settlement event is a no-op.
153
+ UNIQUE (rule_id, purchase_id)
154
+ );
155
+ CREATE INDEX IF NOT EXISTS monetization_entitlements_lookup_idx
156
+ ON monetization_entitlements(consumer_id, kind) WHERE remaining > 0;
157
+ CREATE INDEX IF NOT EXISTS monetization_entitlements_purchase_idx
158
+ ON monetization_entitlements(purchase_id);