payment-kit 1.28.0 → 1.29.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/api/src/crons/index.ts +22 -0
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/libs/audit.ts +38 -8
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/util.ts +13 -0
- package/api/src/queues/event.ts +25 -19
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/payment-methods.ts +130 -0
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +14 -0
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +62 -1
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/blocklet.yml +1 -1
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +1 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/queue.ts +28 -2
- package/cloudflare/shims/sequelize-d1/model.ts +19 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +14 -1
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/worker.ts +59 -4
- package/cloudflare/wrangler.jsonc +7 -1
- package/cloudflare/wrangler.staging.json +2 -1
- package/package.json +10 -6
- package/scripts/seed-google-play.ts +79 -0
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +6 -0
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
import type { LiteralUnion } from 'type-fest';
|
|
4
|
+
|
|
5
|
+
import { createIdGenerator } from '../../libs/util';
|
|
6
|
+
|
|
7
|
+
export const nextEntitlementGrantId = createIdGenerator('eg', 24);
|
|
8
|
+
|
|
9
|
+
// One Subscription (or other source) grants an Entitlement to a Customer.
|
|
10
|
+
// Multiple grants for the same (customer, entitlement) can coexist across channels;
|
|
11
|
+
// the "currently effective" grant is determined by status + active_until.
|
|
12
|
+
// eslint-disable-next-line prettier/prettier
|
|
13
|
+
export class EntitlementGrant extends Model<InferAttributes<EntitlementGrant>, InferCreationAttributes<EntitlementGrant>> {
|
|
14
|
+
declare id: CreationOptional<string>;
|
|
15
|
+
declare livemode: boolean;
|
|
16
|
+
|
|
17
|
+
declare entitlement_id: string;
|
|
18
|
+
declare customer_id: string;
|
|
19
|
+
|
|
20
|
+
declare source_subscription_id?: string;
|
|
21
|
+
declare source_channel: LiteralUnion<
|
|
22
|
+
'stripe' | 'arcblock' | 'ethereum' | 'base' | 'app_store' | 'google_play',
|
|
23
|
+
string
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
declare active_from: number;
|
|
27
|
+
declare active_until: number;
|
|
28
|
+
|
|
29
|
+
declare status: LiteralUnion<'active' | 'canceled' | 'expired' | 'voided', string>;
|
|
30
|
+
|
|
31
|
+
declare metadata?: Record<string, any>;
|
|
32
|
+
|
|
33
|
+
declare created_at: CreationOptional<Date>;
|
|
34
|
+
declare updated_at: CreationOptional<Date>;
|
|
35
|
+
|
|
36
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
37
|
+
id: {
|
|
38
|
+
type: DataTypes.STRING(30),
|
|
39
|
+
primaryKey: true,
|
|
40
|
+
allowNull: false,
|
|
41
|
+
defaultValue: nextEntitlementGrantId,
|
|
42
|
+
},
|
|
43
|
+
livemode: {
|
|
44
|
+
type: DataTypes.BOOLEAN,
|
|
45
|
+
allowNull: false,
|
|
46
|
+
},
|
|
47
|
+
entitlement_id: {
|
|
48
|
+
type: DataTypes.STRING(30),
|
|
49
|
+
allowNull: false,
|
|
50
|
+
},
|
|
51
|
+
customer_id: {
|
|
52
|
+
type: DataTypes.STRING(18),
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
source_subscription_id: {
|
|
56
|
+
type: DataTypes.STRING(30),
|
|
57
|
+
allowNull: true,
|
|
58
|
+
},
|
|
59
|
+
source_channel: {
|
|
60
|
+
type: DataTypes.STRING(20),
|
|
61
|
+
allowNull: false,
|
|
62
|
+
},
|
|
63
|
+
active_from: {
|
|
64
|
+
type: DataTypes.INTEGER,
|
|
65
|
+
allowNull: false,
|
|
66
|
+
},
|
|
67
|
+
active_until: {
|
|
68
|
+
type: DataTypes.INTEGER,
|
|
69
|
+
allowNull: false,
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
type: DataTypes.STRING(20),
|
|
73
|
+
allowNull: false,
|
|
74
|
+
defaultValue: 'active',
|
|
75
|
+
},
|
|
76
|
+
metadata: {
|
|
77
|
+
type: DataTypes.JSON,
|
|
78
|
+
allowNull: true,
|
|
79
|
+
},
|
|
80
|
+
created_at: {
|
|
81
|
+
type: DataTypes.DATE,
|
|
82
|
+
defaultValue: DataTypes.NOW,
|
|
83
|
+
allowNull: false,
|
|
84
|
+
},
|
|
85
|
+
updated_at: {
|
|
86
|
+
type: DataTypes.DATE,
|
|
87
|
+
defaultValue: DataTypes.NOW,
|
|
88
|
+
allowNull: false,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
public static initialize(sequelize: any) {
|
|
93
|
+
this.init(EntitlementGrant.GENESIS_ATTRIBUTES, {
|
|
94
|
+
sequelize,
|
|
95
|
+
modelName: 'EntitlementGrant',
|
|
96
|
+
tableName: 'entitlement_grants',
|
|
97
|
+
createdAt: 'created_at',
|
|
98
|
+
updatedAt: 'updated_at',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public static associate(models: any) {
|
|
103
|
+
this.belongsTo(models.Entitlement, {
|
|
104
|
+
foreignKey: 'entitlement_id',
|
|
105
|
+
as: 'entitlement',
|
|
106
|
+
});
|
|
107
|
+
this.belongsTo(models.Customer, {
|
|
108
|
+
foreignKey: 'customer_id',
|
|
109
|
+
as: 'customer',
|
|
110
|
+
});
|
|
111
|
+
this.belongsTo(models.Subscription, {
|
|
112
|
+
foreignKey: 'source_subscription_id',
|
|
113
|
+
as: 'subscription',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type TEntitlementGrant = InferAttributes<EntitlementGrant>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
// Join table: which products unlock which entitlements (many-to-many)
|
|
5
|
+
// eslint-disable-next-line prettier/prettier
|
|
6
|
+
export class EntitlementProduct extends Model<InferAttributes<EntitlementProduct>, InferCreationAttributes<EntitlementProduct>> {
|
|
7
|
+
declare entitlement_id: string;
|
|
8
|
+
declare product_id: string;
|
|
9
|
+
declare created_at: CreationOptional<Date>;
|
|
10
|
+
declare updated_at: CreationOptional<Date>;
|
|
11
|
+
|
|
12
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
13
|
+
entitlement_id: {
|
|
14
|
+
type: DataTypes.STRING(30),
|
|
15
|
+
primaryKey: true,
|
|
16
|
+
allowNull: false,
|
|
17
|
+
},
|
|
18
|
+
product_id: {
|
|
19
|
+
type: DataTypes.STRING(30),
|
|
20
|
+
primaryKey: true,
|
|
21
|
+
allowNull: false,
|
|
22
|
+
},
|
|
23
|
+
created_at: {
|
|
24
|
+
type: DataTypes.DATE,
|
|
25
|
+
defaultValue: DataTypes.NOW,
|
|
26
|
+
allowNull: false,
|
|
27
|
+
},
|
|
28
|
+
updated_at: {
|
|
29
|
+
type: DataTypes.DATE,
|
|
30
|
+
defaultValue: DataTypes.NOW,
|
|
31
|
+
allowNull: false,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
public static initialize(sequelize: any) {
|
|
36
|
+
this.init(EntitlementProduct.GENESIS_ATTRIBUTES, {
|
|
37
|
+
sequelize,
|
|
38
|
+
modelName: 'EntitlementProduct',
|
|
39
|
+
tableName: 'entitlement_products',
|
|
40
|
+
createdAt: 'created_at',
|
|
41
|
+
updatedAt: 'updated_at',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static associate() {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type TEntitlementProduct = InferAttributes<EntitlementProduct>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
import { createIdGenerator } from '../../libs/util';
|
|
5
|
+
|
|
6
|
+
export const nextEntitlementId = createIdGenerator('ent', 24);
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line prettier/prettier
|
|
9
|
+
export class Entitlement extends Model<InferAttributes<Entitlement>, InferCreationAttributes<Entitlement>> {
|
|
10
|
+
declare id: CreationOptional<string>;
|
|
11
|
+
declare livemode: boolean;
|
|
12
|
+
|
|
13
|
+
declare key: string;
|
|
14
|
+
declare name?: string;
|
|
15
|
+
declare description?: string;
|
|
16
|
+
|
|
17
|
+
declare metadata?: Record<string, any>;
|
|
18
|
+
|
|
19
|
+
declare created_at: CreationOptional<Date>;
|
|
20
|
+
declare updated_at: CreationOptional<Date>;
|
|
21
|
+
|
|
22
|
+
public static readonly GENESIS_ATTRIBUTES = {
|
|
23
|
+
id: {
|
|
24
|
+
type: DataTypes.STRING(30),
|
|
25
|
+
primaryKey: true,
|
|
26
|
+
allowNull: false,
|
|
27
|
+
defaultValue: nextEntitlementId,
|
|
28
|
+
},
|
|
29
|
+
livemode: {
|
|
30
|
+
type: DataTypes.BOOLEAN,
|
|
31
|
+
allowNull: false,
|
|
32
|
+
},
|
|
33
|
+
key: {
|
|
34
|
+
type: DataTypes.STRING(64),
|
|
35
|
+
allowNull: false,
|
|
36
|
+
unique: true,
|
|
37
|
+
},
|
|
38
|
+
name: {
|
|
39
|
+
type: DataTypes.STRING(255),
|
|
40
|
+
allowNull: true,
|
|
41
|
+
},
|
|
42
|
+
description: {
|
|
43
|
+
type: DataTypes.TEXT,
|
|
44
|
+
allowNull: true,
|
|
45
|
+
},
|
|
46
|
+
metadata: {
|
|
47
|
+
type: DataTypes.JSON,
|
|
48
|
+
allowNull: true,
|
|
49
|
+
},
|
|
50
|
+
created_at: {
|
|
51
|
+
type: DataTypes.DATE,
|
|
52
|
+
defaultValue: DataTypes.NOW,
|
|
53
|
+
allowNull: false,
|
|
54
|
+
},
|
|
55
|
+
updated_at: {
|
|
56
|
+
type: DataTypes.DATE,
|
|
57
|
+
defaultValue: DataTypes.NOW,
|
|
58
|
+
allowNull: false,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
public static initialize(sequelize: any) {
|
|
63
|
+
this.init(Entitlement.GENESIS_ATTRIBUTES, {
|
|
64
|
+
sequelize,
|
|
65
|
+
modelName: 'Entitlement',
|
|
66
|
+
tableName: 'entitlements',
|
|
67
|
+
createdAt: 'created_at',
|
|
68
|
+
updatedAt: 'updated_at',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static associate(models: any) {
|
|
73
|
+
this.belongsToMany(models.Product, {
|
|
74
|
+
through: models.EntitlementProduct,
|
|
75
|
+
foreignKey: 'entitlement_id',
|
|
76
|
+
otherKey: 'product_id',
|
|
77
|
+
as: 'products',
|
|
78
|
+
});
|
|
79
|
+
this.hasMany(models.EntitlementGrant, {
|
|
80
|
+
foreignKey: 'entitlement_id',
|
|
81
|
+
as: 'grants',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type TEntitlement = InferAttributes<Entitlement>;
|
|
@@ -39,6 +39,9 @@ import { PriceQuote } from './price-quote';
|
|
|
39
39
|
import { ArchiveMetadata } from './archive-metadata';
|
|
40
40
|
import { ArchiveLock } from './archive-lock';
|
|
41
41
|
import { RevenueSnapshot } from './revenue-snapshot';
|
|
42
|
+
import { Entitlement } from './entitlement';
|
|
43
|
+
import { EntitlementGrant } from './entitlement-grant';
|
|
44
|
+
import { EntitlementProduct } from './entitlement-product';
|
|
42
45
|
|
|
43
46
|
const models = {
|
|
44
47
|
CheckoutSession,
|
|
@@ -81,6 +84,9 @@ const models = {
|
|
|
81
84
|
ArchiveMetadata,
|
|
82
85
|
ArchiveLock,
|
|
83
86
|
RevenueSnapshot,
|
|
87
|
+
Entitlement,
|
|
88
|
+
EntitlementGrant,
|
|
89
|
+
EntitlementProduct,
|
|
84
90
|
};
|
|
85
91
|
|
|
86
92
|
export function initialize(sequelize: any) {
|
|
@@ -137,6 +143,9 @@ export * from './price-quote';
|
|
|
137
143
|
export * from './archive-metadata';
|
|
138
144
|
export * from './archive-lock';
|
|
139
145
|
export * from './revenue-snapshot';
|
|
146
|
+
export * from './entitlement';
|
|
147
|
+
export * from './entitlement-grant';
|
|
148
|
+
export * from './entitlement-product';
|
|
140
149
|
|
|
141
150
|
export type TPriceExpanded = TPrice & {
|
|
142
151
|
object: 'price';
|
|
@@ -84,6 +84,14 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
84
84
|
declare tax: string;
|
|
85
85
|
declare total: string;
|
|
86
86
|
|
|
87
|
+
// Three-segment amounts for cross-channel accounting (D-001 A).
|
|
88
|
+
// gross_amount = what the user actually paid (= total for non-IAP channels).
|
|
89
|
+
// platform_fee = amount Apple / Google withheld (0 for Stripe / on-chain).
|
|
90
|
+
// net_amount = what we actually received (= gross_amount - platform_fee).
|
|
91
|
+
declare gross_amount?: string;
|
|
92
|
+
declare platform_fee?: string;
|
|
93
|
+
declare net_amount?: string;
|
|
94
|
+
|
|
87
95
|
declare amount_due: string; // total - amount_paid
|
|
88
96
|
declare amount_paid: string;
|
|
89
97
|
declare amount_remaining: string; // amount_due - amount_paid
|
|
@@ -250,6 +258,18 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
250
258
|
type: DataTypes.STRING(32),
|
|
251
259
|
allowNull: false,
|
|
252
260
|
},
|
|
261
|
+
gross_amount: {
|
|
262
|
+
type: DataTypes.STRING(32),
|
|
263
|
+
allowNull: true,
|
|
264
|
+
},
|
|
265
|
+
platform_fee: {
|
|
266
|
+
type: DataTypes.STRING(32),
|
|
267
|
+
defaultValue: '0',
|
|
268
|
+
},
|
|
269
|
+
net_amount: {
|
|
270
|
+
type: DataTypes.STRING(32),
|
|
271
|
+
allowNull: true,
|
|
272
|
+
},
|
|
253
273
|
subtotal: {
|
|
254
274
|
type: DataTypes.STRING(32),
|
|
255
275
|
allowNull: false,
|
|
@@ -7,6 +7,8 @@ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes,
|
|
|
7
7
|
import Stripe from 'stripe';
|
|
8
8
|
import type { LiteralUnion } from 'type-fest';
|
|
9
9
|
|
|
10
|
+
import { AppStoreClient } from '../../integrations/app-store/client';
|
|
11
|
+
import { GooglePlayClient } from '../../integrations/google-play/client';
|
|
10
12
|
import { STRIPE_API_VERSION, createIdGenerator } from '../../libs/util';
|
|
11
13
|
import { sequelize } from '../sequelize';
|
|
12
14
|
import type { PaymentMethodSettings } from './types';
|
|
@@ -17,6 +19,8 @@ const nextId = createIdGenerator('pm', 24);
|
|
|
17
19
|
const stripeClients = new Map<string, Stripe>();
|
|
18
20
|
const evmClients = new Map<string, JsonRpcProvider>();
|
|
19
21
|
const ocapClients = new Map<string, OcapClient>();
|
|
22
|
+
const googlePlayClients = new Map<string, GooglePlayClient>();
|
|
23
|
+
const appStoreClients = new Map<string, AppStoreClient>();
|
|
20
24
|
|
|
21
25
|
// eslint-disable-next-line prettier/prettier
|
|
22
26
|
export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCreationAttributes<PaymentMethod>> {
|
|
@@ -27,7 +31,10 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
27
31
|
declare livemode: boolean;
|
|
28
32
|
declare locked: boolean;
|
|
29
33
|
|
|
30
|
-
declare type: LiteralUnion<
|
|
34
|
+
declare type: LiteralUnion<
|
|
35
|
+
'stripe' | 'arcblock' | 'ethereum' | 'bitcoin' | 'base' | 'app_store' | 'google_play',
|
|
36
|
+
string
|
|
37
|
+
>;
|
|
31
38
|
|
|
32
39
|
declare name: string;
|
|
33
40
|
|
|
@@ -153,6 +160,15 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
153
160
|
tmp.stripe.secret_key = security.encrypt(tmp.stripe.secret_key);
|
|
154
161
|
tmp.stripe.webhook_signing_secret = security.encrypt(tmp.stripe.webhook_signing_secret);
|
|
155
162
|
}
|
|
163
|
+
if (tmp.google_play) {
|
|
164
|
+
tmp.google_play.service_account_json = security.encrypt(tmp.google_play.service_account_json);
|
|
165
|
+
}
|
|
166
|
+
if (tmp.app_store?.private_key_pem) {
|
|
167
|
+
tmp.app_store.private_key_pem = security.encrypt(tmp.app_store.private_key_pem);
|
|
168
|
+
}
|
|
169
|
+
if (tmp.app_store?.shared_secret) {
|
|
170
|
+
tmp.app_store.shared_secret = security.encrypt(tmp.app_store.shared_secret);
|
|
171
|
+
}
|
|
156
172
|
|
|
157
173
|
return tmp;
|
|
158
174
|
}
|
|
@@ -163,6 +179,15 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
163
179
|
tmp.stripe.secret_key = security.decrypt(tmp.stripe.secret_key);
|
|
164
180
|
tmp.stripe.webhook_signing_secret = security.decrypt(tmp.stripe.webhook_signing_secret);
|
|
165
181
|
}
|
|
182
|
+
if (tmp.google_play) {
|
|
183
|
+
tmp.google_play.service_account_json = security.decrypt(tmp.google_play.service_account_json);
|
|
184
|
+
}
|
|
185
|
+
if (tmp.app_store?.private_key_pem) {
|
|
186
|
+
tmp.app_store.private_key_pem = security.decrypt(tmp.app_store.private_key_pem);
|
|
187
|
+
}
|
|
188
|
+
if (tmp.app_store?.shared_secret) {
|
|
189
|
+
tmp.app_store.shared_secret = security.decrypt(tmp.app_store.shared_secret);
|
|
190
|
+
}
|
|
166
191
|
|
|
167
192
|
return tmp;
|
|
168
193
|
}
|
|
@@ -240,6 +265,42 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
240
265
|
return client as JsonRpcProvider;
|
|
241
266
|
}
|
|
242
267
|
|
|
268
|
+
getGooglePlayClient() {
|
|
269
|
+
if (this.type !== 'google_play') {
|
|
270
|
+
throw new Error('payment method is not google_play');
|
|
271
|
+
}
|
|
272
|
+
if (!this.settings.google_play) {
|
|
273
|
+
throw new Error('payment method config insufficient for google_play');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (googlePlayClients.has(this.id)) {
|
|
277
|
+
return googlePlayClients.get(this.id) as GooglePlayClient;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const settings = PaymentMethod.decryptSettings(this.settings);
|
|
281
|
+
const client = GooglePlayClient.fromSettings(settings.google_play!);
|
|
282
|
+
googlePlayClients.set(this.id, client);
|
|
283
|
+
return client;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getAppStoreClient() {
|
|
287
|
+
if (this.type !== 'app_store') {
|
|
288
|
+
throw new Error('payment method is not app_store');
|
|
289
|
+
}
|
|
290
|
+
if (!this.settings.app_store) {
|
|
291
|
+
throw new Error('payment method config insufficient for app_store');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (appStoreClients.has(this.id)) {
|
|
295
|
+
return appStoreClients.get(this.id) as AppStoreClient;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const settings = PaymentMethod.decryptSettings(this.settings);
|
|
299
|
+
const client = AppStoreClient.fromSettings(settings.app_store!);
|
|
300
|
+
appStoreClients.set(this.id, client);
|
|
301
|
+
return client;
|
|
302
|
+
}
|
|
303
|
+
|
|
243
304
|
public static async supportAutoCharge(id: string) {
|
|
244
305
|
const method = await PaymentMethod.findByPk(id);
|
|
245
306
|
return method && CHARGE_SUPPORTED_CHAIN_TYPES.includes(method.type);
|
|
@@ -68,6 +68,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
68
68
|
declare updated_at: CreationOptional<Date>;
|
|
69
69
|
declare type: LiteralUnion<'refund' | 'stake_return', string>;
|
|
70
70
|
|
|
71
|
+
// Refund origin (added for IAP support).
|
|
72
|
+
// merchant_initiated —商户主动调 API 退款 (Stripe / 链上)
|
|
73
|
+
// platform_initiated — Apple/Google webhook 推过来的退款,我们被动接收
|
|
74
|
+
declare source?: LiteralUnion<'merchant_initiated' | 'platform_initiated', string>;
|
|
75
|
+
|
|
71
76
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
72
77
|
id: {
|
|
73
78
|
type: DataTypes.STRING(30),
|
|
@@ -192,6 +197,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
192
197
|
allowNull: true,
|
|
193
198
|
defaultValue: 'refund',
|
|
194
199
|
},
|
|
200
|
+
source: {
|
|
201
|
+
type: DataTypes.STRING(30),
|
|
202
|
+
allowNull: true,
|
|
203
|
+
defaultValue: 'merchant_initiated',
|
|
204
|
+
},
|
|
195
205
|
},
|
|
196
206
|
{
|
|
197
207
|
sequelize,
|
|
@@ -129,6 +129,11 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
129
129
|
|
|
130
130
|
declare recovered_from?: string;
|
|
131
131
|
|
|
132
|
+
// Payment channel (D-005). Stripe / arcblock / ethereum / base / app_store / google_play
|
|
133
|
+
declare channel?: LiteralUnion<'stripe' | 'arcblock' | 'ethereum' | 'base' | 'app_store' | 'google_play', string>;
|
|
134
|
+
// IAP platform environment (D-005). Orthogonal to livemode.
|
|
135
|
+
declare environment?: LiteralUnion<'production' | 'sandbox', string>;
|
|
136
|
+
|
|
132
137
|
// TODO: following fields not supported
|
|
133
138
|
// application
|
|
134
139
|
// application_fee_percent
|
|
@@ -346,6 +351,15 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
346
351
|
allowNull: true,
|
|
347
352
|
defaultValue: null,
|
|
348
353
|
},
|
|
354
|
+
channel: {
|
|
355
|
+
type: DataTypes.STRING(20),
|
|
356
|
+
allowNull: true,
|
|
357
|
+
},
|
|
358
|
+
environment: {
|
|
359
|
+
type: DataTypes.STRING(20),
|
|
360
|
+
allowNull: true,
|
|
361
|
+
defaultValue: 'production',
|
|
362
|
+
},
|
|
349
363
|
},
|
|
350
364
|
{
|
|
351
365
|
sequelize,
|
|
@@ -310,6 +310,22 @@ export type PaymentMethodSettings = {
|
|
|
310
310
|
native_symbol: string;
|
|
311
311
|
confirmation: number;
|
|
312
312
|
};
|
|
313
|
+
google_play?: {
|
|
314
|
+
package_name: string;
|
|
315
|
+
service_account_json: string; // encrypted JSON string of GCP service account credentials
|
|
316
|
+
pubsub_topic_name: string; // projects/<project>/topics/<topic>
|
|
317
|
+
};
|
|
318
|
+
app_store?: {
|
|
319
|
+
bundle_id: string;
|
|
320
|
+
environment: LiteralUnion<'production' | 'sandbox', string>;
|
|
321
|
+
/** App-Specific Shared Secret — only needed when verifying StoreKit 1 (legacy) receipts via Apple's verifyReceipt endpoint. StoreKit 2 JWS path does not use this. */
|
|
322
|
+
shared_secret?: string; // encrypted
|
|
323
|
+
// App Store Server API credentials — only needed when calling Apple back for state refresh.
|
|
324
|
+
// StoreKit 2 JWS verification (the happy path) does NOT need any of these.
|
|
325
|
+
issuer_id?: string;
|
|
326
|
+
key_id?: string;
|
|
327
|
+
private_key_pem?: string; // encrypted .p8 file contents
|
|
328
|
+
};
|
|
313
329
|
};
|
|
314
330
|
|
|
315
331
|
export type VaultConfig = {
|
|
@@ -366,6 +382,22 @@ export type PaymentDetails = {
|
|
|
366
382
|
block_height: number;
|
|
367
383
|
confirmations: number;
|
|
368
384
|
};
|
|
385
|
+
google_play?: {
|
|
386
|
+
purchase_token: string;
|
|
387
|
+
order_id?: string;
|
|
388
|
+
product_id: string;
|
|
389
|
+
subscription_id?: string;
|
|
390
|
+
expiry_time_millis?: string;
|
|
391
|
+
environment?: LiteralUnion<'production' | 'sandbox', string>;
|
|
392
|
+
};
|
|
393
|
+
app_store?: {
|
|
394
|
+
original_transaction_id: string;
|
|
395
|
+
transaction_id?: string;
|
|
396
|
+
product_id: string;
|
|
397
|
+
web_order_line_item_id?: string;
|
|
398
|
+
environment?: LiteralUnion<'Production' | 'Sandbox', string>;
|
|
399
|
+
expires_at?: number; // unix seconds
|
|
400
|
+
};
|
|
369
401
|
};
|
|
370
402
|
|
|
371
403
|
export type PaymentBeneficiary = {
|