@uniforge/core 0.1.0-alpha.2
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/dist/auth/index.d.cts +165 -0
- package/dist/auth/index.d.ts +165 -0
- package/dist/auth/index.js +443 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +406 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/billing/index.d.cts +34 -0
- package/dist/billing/index.d.ts +34 -0
- package/dist/billing/index.js +254 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/index.mjs +225 -0
- package/dist/billing/index.mjs.map +1 -0
- package/dist/config/index.d.cts +12 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.js +186 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +156 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/database/index.d.cts +33 -0
- package/dist/database/index.d.ts +33 -0
- package/dist/database/index.js +127 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/index.mjs +95 -0
- package/dist/database/index.mjs.map +1 -0
- package/dist/graphql/index.d.cts +36 -0
- package/dist/graphql/index.d.ts +36 -0
- package/dist/graphql/index.js +209 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/index.mjs +179 -0
- package/dist/graphql/index.mjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-store/index.d.cts +11 -0
- package/dist/multi-store/index.d.ts +11 -0
- package/dist/multi-store/index.js +473 -0
- package/dist/multi-store/index.js.map +1 -0
- package/dist/multi-store/index.mjs +447 -0
- package/dist/multi-store/index.mjs.map +1 -0
- package/dist/multi-tenant/index.d.cts +23 -0
- package/dist/multi-tenant/index.d.ts +23 -0
- package/dist/multi-tenant/index.js +69 -0
- package/dist/multi-tenant/index.js.map +1 -0
- package/dist/multi-tenant/index.mjs +41 -0
- package/dist/multi-tenant/index.mjs.map +1 -0
- package/dist/performance/index.d.cts +34 -0
- package/dist/performance/index.d.ts +34 -0
- package/dist/performance/index.js +319 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/index.mjs +290 -0
- package/dist/performance/index.mjs.map +1 -0
- package/dist/platform/index.d.cts +25 -0
- package/dist/platform/index.d.ts +25 -0
- package/dist/platform/index.js +91 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.mjs +62 -0
- package/dist/platform/index.mjs.map +1 -0
- package/dist/rbac/index.d.cts +24 -0
- package/dist/rbac/index.d.ts +24 -0
- package/dist/rbac/index.js +267 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/rbac/index.mjs +236 -0
- package/dist/rbac/index.mjs.map +1 -0
- package/dist/schema-CM7mHj_H.d.cts +53 -0
- package/dist/schema-CM7mHj_H.d.ts +53 -0
- package/dist/security/index.d.cts +47 -0
- package/dist/security/index.d.ts +47 -0
- package/dist/security/index.js +505 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/index.mjs +474 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/session-storage/index.d.cts +70 -0
- package/dist/session-storage/index.d.ts +70 -0
- package/dist/session-storage/index.js +271 -0
- package/dist/session-storage/index.js.map +1 -0
- package/dist/session-storage/index.mjs +242 -0
- package/dist/session-storage/index.mjs.map +1 -0
- package/dist/webhooks/index.d.cts +89 -0
- package/dist/webhooks/index.d.ts +89 -0
- package/dist/webhooks/index.js +380 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/index.mjs +348 -0
- package/dist/webhooks/index.mjs.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/billing/index.ts
|
|
21
|
+
var billing_exports = {};
|
|
22
|
+
__export(billing_exports, {
|
|
23
|
+
createBillingService: () => createBillingService,
|
|
24
|
+
createFeatureGateMiddleware: () => createFeatureGateMiddleware,
|
|
25
|
+
createPlanRegistry: () => createPlanRegistry
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(billing_exports);
|
|
28
|
+
|
|
29
|
+
// src/billing/service.ts
|
|
30
|
+
var import_client = require("@prisma/client");
|
|
31
|
+
function createBillingService(prisma) {
|
|
32
|
+
return {
|
|
33
|
+
async createSubscription(input) {
|
|
34
|
+
const data = {
|
|
35
|
+
shopDomain: input.shopDomain,
|
|
36
|
+
planName: input.planName,
|
|
37
|
+
status: "PENDING"
|
|
38
|
+
};
|
|
39
|
+
if (input.shopifySubscriptionId) {
|
|
40
|
+
data.shopifySubscriptionId = input.shopifySubscriptionId;
|
|
41
|
+
}
|
|
42
|
+
if (input.confirmationUrl) {
|
|
43
|
+
data.confirmationUrl = input.confirmationUrl;
|
|
44
|
+
}
|
|
45
|
+
if (input.trialDays) {
|
|
46
|
+
const trialEnd = /* @__PURE__ */ new Date();
|
|
47
|
+
trialEnd.setDate(trialEnd.getDate() + input.trialDays);
|
|
48
|
+
data.trialEndsAt = trialEnd;
|
|
49
|
+
}
|
|
50
|
+
const row = await prisma.subscription.create({ data });
|
|
51
|
+
return mapSubscription(row);
|
|
52
|
+
},
|
|
53
|
+
async getSubscription(id) {
|
|
54
|
+
const row = await prisma.subscription.findUnique({ where: { id } });
|
|
55
|
+
return row ? mapSubscription(row) : null;
|
|
56
|
+
},
|
|
57
|
+
async getActiveSubscription(shopDomain) {
|
|
58
|
+
const row = await prisma.subscription.findFirst({
|
|
59
|
+
where: { shopDomain, status: "ACTIVE" },
|
|
60
|
+
orderBy: { createdAt: "desc" }
|
|
61
|
+
});
|
|
62
|
+
return row ? mapSubscription(row) : null;
|
|
63
|
+
},
|
|
64
|
+
async listSubscriptions(shopDomain) {
|
|
65
|
+
const rows = await prisma.subscription.findMany({
|
|
66
|
+
where: { shopDomain },
|
|
67
|
+
orderBy: { createdAt: "desc" }
|
|
68
|
+
});
|
|
69
|
+
return rows.map(mapSubscription);
|
|
70
|
+
},
|
|
71
|
+
async updateSubscriptionStatus(id, status) {
|
|
72
|
+
const row = await prisma.subscription.update({
|
|
73
|
+
where: { id },
|
|
74
|
+
data: { status }
|
|
75
|
+
});
|
|
76
|
+
return mapSubscription(row);
|
|
77
|
+
},
|
|
78
|
+
async updateSubscriptionByShopifyId(shopifySubscriptionId, updates) {
|
|
79
|
+
const data = {};
|
|
80
|
+
if (updates.status) {
|
|
81
|
+
data.status = updates.status;
|
|
82
|
+
}
|
|
83
|
+
if (updates.currentPeriodEnd) {
|
|
84
|
+
data.currentPeriodEnd = updates.currentPeriodEnd;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const row = await prisma.subscription.update({
|
|
88
|
+
where: { shopifySubscriptionId },
|
|
89
|
+
data
|
|
90
|
+
});
|
|
91
|
+
return mapSubscription(row);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof import_client.Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async cancelSubscription(id) {
|
|
100
|
+
const row = await prisma.subscription.update({
|
|
101
|
+
where: { id },
|
|
102
|
+
data: { status: "CANCELLED" }
|
|
103
|
+
});
|
|
104
|
+
return mapSubscription(row);
|
|
105
|
+
},
|
|
106
|
+
async createUsageRecord(input) {
|
|
107
|
+
const data = {
|
|
108
|
+
subscriptionId: input.subscriptionId,
|
|
109
|
+
description: input.description,
|
|
110
|
+
amount: input.amount
|
|
111
|
+
};
|
|
112
|
+
if (input.currencyCode) {
|
|
113
|
+
data.currencyCode = input.currencyCode;
|
|
114
|
+
}
|
|
115
|
+
if (input.idempotencyKey) {
|
|
116
|
+
data.idempotencyKey = input.idempotencyKey;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const row = await prisma.usageRecord.create({ data });
|
|
120
|
+
return mapUsageRecord(row);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof import_client.Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Duplicate idempotency key: ${input.idempotencyKey}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
async getUsageRecords(filter) {
|
|
131
|
+
const where = buildUsageWhere(filter);
|
|
132
|
+
const rows = await prisma.usageRecord.findMany({
|
|
133
|
+
where,
|
|
134
|
+
orderBy: { createdAt: "desc" }
|
|
135
|
+
});
|
|
136
|
+
return rows.map(mapUsageRecord);
|
|
137
|
+
},
|
|
138
|
+
async calculateTotalUsage(filter) {
|
|
139
|
+
const where = buildUsageWhere(filter);
|
|
140
|
+
const result = await prisma.usageRecord.aggregate({
|
|
141
|
+
where,
|
|
142
|
+
_sum: { amount: true }
|
|
143
|
+
});
|
|
144
|
+
return result._sum.amount?.toString() ?? "0";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function buildUsageWhere(filter) {
|
|
149
|
+
const where = {
|
|
150
|
+
subscriptionId: filter.subscriptionId
|
|
151
|
+
};
|
|
152
|
+
if (filter.startDate || filter.endDate) {
|
|
153
|
+
const createdAt = {};
|
|
154
|
+
if (filter.startDate) {
|
|
155
|
+
createdAt.gte = filter.startDate;
|
|
156
|
+
}
|
|
157
|
+
if (filter.endDate) {
|
|
158
|
+
createdAt.lte = filter.endDate;
|
|
159
|
+
}
|
|
160
|
+
where.createdAt = createdAt;
|
|
161
|
+
}
|
|
162
|
+
return where;
|
|
163
|
+
}
|
|
164
|
+
function mapSubscription(row) {
|
|
165
|
+
return {
|
|
166
|
+
id: row.id,
|
|
167
|
+
shopDomain: row.shopDomain,
|
|
168
|
+
shopifySubscriptionId: row.shopifySubscriptionId ?? null,
|
|
169
|
+
planName: row.planName,
|
|
170
|
+
status: row.status,
|
|
171
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
172
|
+
trialEndsAt: row.trialEndsAt ?? null,
|
|
173
|
+
confirmationUrl: row.confirmationUrl ?? null,
|
|
174
|
+
createdAt: row.createdAt,
|
|
175
|
+
updatedAt: row.updatedAt
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function mapUsageRecord(row) {
|
|
179
|
+
return {
|
|
180
|
+
id: row.id,
|
|
181
|
+
subscriptionId: row.subscriptionId,
|
|
182
|
+
description: row.description,
|
|
183
|
+
amount: row.amount.toString(),
|
|
184
|
+
currencyCode: row.currencyCode,
|
|
185
|
+
idempotencyKey: row.idempotencyKey ?? null,
|
|
186
|
+
createdAt: row.createdAt
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/billing/plans.ts
|
|
191
|
+
function createPlanRegistry() {
|
|
192
|
+
const plans = /* @__PURE__ */ new Map();
|
|
193
|
+
return {
|
|
194
|
+
register(plan) {
|
|
195
|
+
plans.set(plan.name, plan);
|
|
196
|
+
},
|
|
197
|
+
get(name) {
|
|
198
|
+
return plans.get(name);
|
|
199
|
+
},
|
|
200
|
+
list() {
|
|
201
|
+
return [...plans.values()].sort((a, b) => {
|
|
202
|
+
const aOrder = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
203
|
+
const bOrder = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
204
|
+
return aOrder - bOrder;
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
getDefault() {
|
|
208
|
+
for (const plan of plans.values()) {
|
|
209
|
+
if (plan.isDefault) return plan;
|
|
210
|
+
}
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/billing/feature-gate.ts
|
|
217
|
+
var import_billing = require("@uniforge/platform-core/billing");
|
|
218
|
+
function createFeatureGateMiddleware(config) {
|
|
219
|
+
const freeFeatures = config.freeFeatures ?? [];
|
|
220
|
+
return {
|
|
221
|
+
async hasFeature(shopDomain, featureName) {
|
|
222
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
223
|
+
if (!plan) {
|
|
224
|
+
return freeFeatures.includes(featureName);
|
|
225
|
+
}
|
|
226
|
+
const planFeatures = config.planFeatures[plan.name] ?? [];
|
|
227
|
+
return planFeatures.includes(featureName) || freeFeatures.includes(featureName);
|
|
228
|
+
},
|
|
229
|
+
async requireFeature(shopDomain, featureName) {
|
|
230
|
+
const has = await this.hasFeature(shopDomain, featureName);
|
|
231
|
+
if (!has) {
|
|
232
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
233
|
+
throw new import_billing.FeatureGateError(featureName, plan?.name ?? null);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async getAvailableFeatures(shopDomain) {
|
|
237
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
238
|
+
const planFeatures = plan ? config.planFeatures[plan.name] ?? [] : [];
|
|
239
|
+
const combined = /* @__PURE__ */ new Set([...planFeatures, ...freeFeatures]);
|
|
240
|
+
return [...combined];
|
|
241
|
+
},
|
|
242
|
+
async getCurrentPlanName(shopDomain) {
|
|
243
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
244
|
+
return plan?.name ?? null;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
249
|
+
0 && (module.exports = {
|
|
250
|
+
createBillingService,
|
|
251
|
+
createFeatureGateMiddleware,
|
|
252
|
+
createPlanRegistry
|
|
253
|
+
});
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/index.ts","../../src/billing/service.ts","../../src/billing/plans.ts","../../src/billing/feature-gate.ts"],"sourcesContent":["/**\n * @uniforge/core/billing\n *\n * Core billing service, plan registry, and feature gate middleware.\n */\n\nexport { createBillingService } from './service.js';\nexport { createPlanRegistry } from './plans.js';\nexport { createFeatureGateMiddleware } from './feature-gate.js';\n","/**\n * Core billing service implementation with Prisma persistence.\n *\n * Provides subscription management and usage record tracking\n * using the PrismaClient for database operations.\n */\n\nimport { Prisma } from '@prisma/client';\nimport type { PrismaClient } from '@prisma/client';\nimport type {\n BillingService,\n} from '@uniforge/platform-core/billing';\nimport type {\n CreateSubscriptionInput,\n CreateUsageRecordInput,\n Subscription,\n SubscriptionStatus,\n UsageRecord,\n UsageRecordFilter,\n} from '@uniforge/platform-core/billing';\n\n/** Create a BillingService backed by Prisma. */\nexport function createBillingService(prisma: PrismaClient): BillingService {\n return {\n async createSubscription(input: CreateSubscriptionInput): Promise<Subscription> {\n const data: Prisma.SubscriptionUncheckedCreateInput = {\n shopDomain: input.shopDomain,\n planName: input.planName,\n status: 'PENDING',\n };\n if (input.shopifySubscriptionId) {\n data.shopifySubscriptionId = input.shopifySubscriptionId;\n }\n if (input.confirmationUrl) {\n data.confirmationUrl = input.confirmationUrl;\n }\n if (input.trialDays) {\n const trialEnd = new Date();\n trialEnd.setDate(trialEnd.getDate() + input.trialDays);\n data.trialEndsAt = trialEnd;\n }\n\n const row = await prisma.subscription.create({ data });\n return mapSubscription(row);\n },\n\n async getSubscription(id: string): Promise<Subscription | null> {\n const row = await prisma.subscription.findUnique({ where: { id } });\n return row ? mapSubscription(row) : null;\n },\n\n async getActiveSubscription(shopDomain: string): Promise<Subscription | null> {\n const row = await prisma.subscription.findFirst({\n where: { shopDomain, status: 'ACTIVE' },\n orderBy: { createdAt: 'desc' },\n });\n return row ? mapSubscription(row) : null;\n },\n\n async listSubscriptions(shopDomain: string): Promise<Subscription[]> {\n const rows = await prisma.subscription.findMany({\n where: { shopDomain },\n orderBy: { createdAt: 'desc' },\n });\n return rows.map(mapSubscription);\n },\n\n async updateSubscriptionStatus(\n id: string,\n status: SubscriptionStatus,\n ): Promise<Subscription> {\n const row = await prisma.subscription.update({\n where: { id },\n data: { status },\n });\n return mapSubscription(row);\n },\n\n async updateSubscriptionByShopifyId(\n shopifySubscriptionId: string,\n updates: Partial<Pick<Subscription, 'status' | 'currentPeriodEnd'>>,\n ): Promise<Subscription | null> {\n const data: Prisma.SubscriptionUpdateInput = {};\n if (updates.status) {\n data.status = updates.status;\n }\n if (updates.currentPeriodEnd) {\n data.currentPeriodEnd = updates.currentPeriodEnd;\n }\n\n try {\n const row = await prisma.subscription.update({\n where: { shopifySubscriptionId },\n data,\n });\n return mapSubscription(row);\n } catch (error: unknown) {\n if (\n error instanceof Prisma.PrismaClientKnownRequestError &&\n error.code === 'P2025'\n ) {\n return null;\n }\n throw error;\n }\n },\n\n async cancelSubscription(id: string): Promise<Subscription> {\n const row = await prisma.subscription.update({\n where: { id },\n data: { status: 'CANCELLED' },\n });\n return mapSubscription(row);\n },\n\n async createUsageRecord(input: CreateUsageRecordInput): Promise<UsageRecord> {\n const data: Prisma.UsageRecordUncheckedCreateInput = {\n subscriptionId: input.subscriptionId,\n description: input.description,\n amount: input.amount,\n };\n if (input.currencyCode) {\n data.currencyCode = input.currencyCode;\n }\n if (input.idempotencyKey) {\n data.idempotencyKey = input.idempotencyKey;\n }\n\n try {\n const row = await prisma.usageRecord.create({ data });\n return mapUsageRecord(row);\n } catch (error: unknown) {\n if (\n error instanceof Prisma.PrismaClientKnownRequestError &&\n error.code === 'P2002'\n ) {\n throw new Error(\n `Duplicate idempotency key: ${input.idempotencyKey}`,\n );\n }\n throw error;\n }\n },\n\n async getUsageRecords(filter: UsageRecordFilter): Promise<UsageRecord[]> {\n const where = buildUsageWhere(filter);\n const rows = await prisma.usageRecord.findMany({\n where,\n orderBy: { createdAt: 'desc' },\n });\n return rows.map(mapUsageRecord);\n },\n\n async calculateTotalUsage(filter: UsageRecordFilter): Promise<string> {\n const where = buildUsageWhere(filter);\n const result = await prisma.usageRecord.aggregate({\n where,\n _sum: { amount: true },\n });\n return result._sum.amount?.toString() ?? '0';\n },\n };\n}\n\nfunction buildUsageWhere(filter: UsageRecordFilter): Prisma.UsageRecordWhereInput {\n const where: Prisma.UsageRecordWhereInput = {\n subscriptionId: filter.subscriptionId,\n };\n if (filter.startDate || filter.endDate) {\n const createdAt: { gte?: Date; lte?: Date } = {};\n if (filter.startDate) {\n createdAt.gte = filter.startDate;\n }\n if (filter.endDate) {\n createdAt.lte = filter.endDate;\n }\n where.createdAt = createdAt;\n }\n return where;\n}\n\n/** Row shape returned by Prisma for the Subscription model. */\ninterface SubscriptionRow {\n id: string;\n shopDomain: string;\n shopifySubscriptionId: string | null;\n planName: string;\n status: string;\n currentPeriodEnd: Date | null;\n trialEndsAt: Date | null;\n confirmationUrl: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\n/** Row shape returned by Prisma for the UsageRecord model. */\ninterface UsageRecordRow {\n id: string;\n subscriptionId: string;\n description: string;\n amount: Prisma.Decimal | number;\n currencyCode: string;\n idempotencyKey: string | null;\n createdAt: Date;\n}\n\nfunction mapSubscription(row: SubscriptionRow): Subscription {\n return {\n id: row.id,\n shopDomain: row.shopDomain,\n shopifySubscriptionId: row.shopifySubscriptionId ?? null,\n planName: row.planName,\n status: row.status as SubscriptionStatus,\n currentPeriodEnd: row.currentPeriodEnd ?? null,\n trialEndsAt: row.trialEndsAt ?? null,\n confirmationUrl: row.confirmationUrl ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction mapUsageRecord(row: UsageRecordRow): UsageRecord {\n return {\n id: row.id,\n subscriptionId: row.subscriptionId,\n description: row.description,\n amount: row.amount.toString(),\n currencyCode: row.currencyCode,\n idempotencyKey: row.idempotencyKey ?? null,\n createdAt: row.createdAt,\n };\n}\n","/**\n * Billing plan registry.\n *\n * In-memory registry for managing available billing plans,\n * following the same pattern as the webhook handler registry.\n */\n\nimport type { BillingPlan, PlanRegistry } from '@uniforge/platform-core/billing';\n\n/** Create a PlanRegistry that stores plans in memory. */\nexport function createPlanRegistry(): PlanRegistry {\n const plans = new Map<string, BillingPlan>();\n\n return {\n register(plan: BillingPlan): void {\n plans.set(plan.name, plan);\n },\n\n get(name: string): BillingPlan | undefined {\n return plans.get(name);\n },\n\n list(): BillingPlan[] {\n return [...plans.values()].sort((a, b) => {\n const aOrder = a.sortOrder ?? Number.MAX_SAFE_INTEGER;\n const bOrder = b.sortOrder ?? Number.MAX_SAFE_INTEGER;\n return aOrder - bOrder;\n });\n },\n\n getDefault(): BillingPlan | undefined {\n for (const plan of plans.values()) {\n if (plan.isDefault) return plan;\n }\n return undefined;\n },\n };\n}\n","/**\n * Feature gate middleware for plan-based access control.\n *\n * Checks whether a shop's active billing plan includes a given feature.\n * Falls back to free features when no active plan exists.\n */\n\nimport type {\n FeatureGateConfig,\n FeatureGateMiddleware,\n} from '@uniforge/platform-core/billing';\nimport { FeatureGateError } from '@uniforge/platform-core/billing';\n\n/** Create a FeatureGateMiddleware from the given configuration. */\nexport function createFeatureGateMiddleware(\n config: FeatureGateConfig,\n): FeatureGateMiddleware {\n const freeFeatures = config.freeFeatures ?? [];\n\n return {\n async hasFeature(shopDomain: string, featureName: string): Promise<boolean> {\n const plan = await config.getPlanForShop(shopDomain);\n if (!plan) {\n return freeFeatures.includes(featureName);\n }\n const planFeatures = config.planFeatures[plan.name] ?? [];\n return planFeatures.includes(featureName) || freeFeatures.includes(featureName);\n },\n\n async requireFeature(shopDomain: string, featureName: string): Promise<void> {\n const has = await this.hasFeature(shopDomain, featureName);\n if (!has) {\n const plan = await config.getPlanForShop(shopDomain);\n throw new FeatureGateError(featureName, plan?.name ?? null);\n }\n },\n\n async getAvailableFeatures(shopDomain: string): Promise<string[]> {\n const plan = await config.getPlanForShop(shopDomain);\n const planFeatures = plan ? (config.planFeatures[plan.name] ?? []) : [];\n const combined = new Set([...planFeatures, ...freeFeatures]);\n return [...combined];\n },\n\n async getCurrentPlanName(shopDomain: string): Promise<string | null> {\n const plan = await config.getPlanForShop(shopDomain);\n return plan?.name ?? null;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/billing/service.ts
|
|
2
|
+
import { Prisma } from "@prisma/client";
|
|
3
|
+
function createBillingService(prisma) {
|
|
4
|
+
return {
|
|
5
|
+
async createSubscription(input) {
|
|
6
|
+
const data = {
|
|
7
|
+
shopDomain: input.shopDomain,
|
|
8
|
+
planName: input.planName,
|
|
9
|
+
status: "PENDING"
|
|
10
|
+
};
|
|
11
|
+
if (input.shopifySubscriptionId) {
|
|
12
|
+
data.shopifySubscriptionId = input.shopifySubscriptionId;
|
|
13
|
+
}
|
|
14
|
+
if (input.confirmationUrl) {
|
|
15
|
+
data.confirmationUrl = input.confirmationUrl;
|
|
16
|
+
}
|
|
17
|
+
if (input.trialDays) {
|
|
18
|
+
const trialEnd = /* @__PURE__ */ new Date();
|
|
19
|
+
trialEnd.setDate(trialEnd.getDate() + input.trialDays);
|
|
20
|
+
data.trialEndsAt = trialEnd;
|
|
21
|
+
}
|
|
22
|
+
const row = await prisma.subscription.create({ data });
|
|
23
|
+
return mapSubscription(row);
|
|
24
|
+
},
|
|
25
|
+
async getSubscription(id) {
|
|
26
|
+
const row = await prisma.subscription.findUnique({ where: { id } });
|
|
27
|
+
return row ? mapSubscription(row) : null;
|
|
28
|
+
},
|
|
29
|
+
async getActiveSubscription(shopDomain) {
|
|
30
|
+
const row = await prisma.subscription.findFirst({
|
|
31
|
+
where: { shopDomain, status: "ACTIVE" },
|
|
32
|
+
orderBy: { createdAt: "desc" }
|
|
33
|
+
});
|
|
34
|
+
return row ? mapSubscription(row) : null;
|
|
35
|
+
},
|
|
36
|
+
async listSubscriptions(shopDomain) {
|
|
37
|
+
const rows = await prisma.subscription.findMany({
|
|
38
|
+
where: { shopDomain },
|
|
39
|
+
orderBy: { createdAt: "desc" }
|
|
40
|
+
});
|
|
41
|
+
return rows.map(mapSubscription);
|
|
42
|
+
},
|
|
43
|
+
async updateSubscriptionStatus(id, status) {
|
|
44
|
+
const row = await prisma.subscription.update({
|
|
45
|
+
where: { id },
|
|
46
|
+
data: { status }
|
|
47
|
+
});
|
|
48
|
+
return mapSubscription(row);
|
|
49
|
+
},
|
|
50
|
+
async updateSubscriptionByShopifyId(shopifySubscriptionId, updates) {
|
|
51
|
+
const data = {};
|
|
52
|
+
if (updates.status) {
|
|
53
|
+
data.status = updates.status;
|
|
54
|
+
}
|
|
55
|
+
if (updates.currentPeriodEnd) {
|
|
56
|
+
data.currentPeriodEnd = updates.currentPeriodEnd;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const row = await prisma.subscription.update({
|
|
60
|
+
where: { shopifySubscriptionId },
|
|
61
|
+
data
|
|
62
|
+
});
|
|
63
|
+
return mapSubscription(row);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async cancelSubscription(id) {
|
|
72
|
+
const row = await prisma.subscription.update({
|
|
73
|
+
where: { id },
|
|
74
|
+
data: { status: "CANCELLED" }
|
|
75
|
+
});
|
|
76
|
+
return mapSubscription(row);
|
|
77
|
+
},
|
|
78
|
+
async createUsageRecord(input) {
|
|
79
|
+
const data = {
|
|
80
|
+
subscriptionId: input.subscriptionId,
|
|
81
|
+
description: input.description,
|
|
82
|
+
amount: input.amount
|
|
83
|
+
};
|
|
84
|
+
if (input.currencyCode) {
|
|
85
|
+
data.currencyCode = input.currencyCode;
|
|
86
|
+
}
|
|
87
|
+
if (input.idempotencyKey) {
|
|
88
|
+
data.idempotencyKey = input.idempotencyKey;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const row = await prisma.usageRecord.create({ data });
|
|
92
|
+
return mapUsageRecord(row);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Duplicate idempotency key: ${input.idempotencyKey}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
async getUsageRecords(filter) {
|
|
103
|
+
const where = buildUsageWhere(filter);
|
|
104
|
+
const rows = await prisma.usageRecord.findMany({
|
|
105
|
+
where,
|
|
106
|
+
orderBy: { createdAt: "desc" }
|
|
107
|
+
});
|
|
108
|
+
return rows.map(mapUsageRecord);
|
|
109
|
+
},
|
|
110
|
+
async calculateTotalUsage(filter) {
|
|
111
|
+
const where = buildUsageWhere(filter);
|
|
112
|
+
const result = await prisma.usageRecord.aggregate({
|
|
113
|
+
where,
|
|
114
|
+
_sum: { amount: true }
|
|
115
|
+
});
|
|
116
|
+
return result._sum.amount?.toString() ?? "0";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function buildUsageWhere(filter) {
|
|
121
|
+
const where = {
|
|
122
|
+
subscriptionId: filter.subscriptionId
|
|
123
|
+
};
|
|
124
|
+
if (filter.startDate || filter.endDate) {
|
|
125
|
+
const createdAt = {};
|
|
126
|
+
if (filter.startDate) {
|
|
127
|
+
createdAt.gte = filter.startDate;
|
|
128
|
+
}
|
|
129
|
+
if (filter.endDate) {
|
|
130
|
+
createdAt.lte = filter.endDate;
|
|
131
|
+
}
|
|
132
|
+
where.createdAt = createdAt;
|
|
133
|
+
}
|
|
134
|
+
return where;
|
|
135
|
+
}
|
|
136
|
+
function mapSubscription(row) {
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
shopDomain: row.shopDomain,
|
|
140
|
+
shopifySubscriptionId: row.shopifySubscriptionId ?? null,
|
|
141
|
+
planName: row.planName,
|
|
142
|
+
status: row.status,
|
|
143
|
+
currentPeriodEnd: row.currentPeriodEnd ?? null,
|
|
144
|
+
trialEndsAt: row.trialEndsAt ?? null,
|
|
145
|
+
confirmationUrl: row.confirmationUrl ?? null,
|
|
146
|
+
createdAt: row.createdAt,
|
|
147
|
+
updatedAt: row.updatedAt
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function mapUsageRecord(row) {
|
|
151
|
+
return {
|
|
152
|
+
id: row.id,
|
|
153
|
+
subscriptionId: row.subscriptionId,
|
|
154
|
+
description: row.description,
|
|
155
|
+
amount: row.amount.toString(),
|
|
156
|
+
currencyCode: row.currencyCode,
|
|
157
|
+
idempotencyKey: row.idempotencyKey ?? null,
|
|
158
|
+
createdAt: row.createdAt
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/billing/plans.ts
|
|
163
|
+
function createPlanRegistry() {
|
|
164
|
+
const plans = /* @__PURE__ */ new Map();
|
|
165
|
+
return {
|
|
166
|
+
register(plan) {
|
|
167
|
+
plans.set(plan.name, plan);
|
|
168
|
+
},
|
|
169
|
+
get(name) {
|
|
170
|
+
return plans.get(name);
|
|
171
|
+
},
|
|
172
|
+
list() {
|
|
173
|
+
return [...plans.values()].sort((a, b) => {
|
|
174
|
+
const aOrder = a.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
175
|
+
const bOrder = b.sortOrder ?? Number.MAX_SAFE_INTEGER;
|
|
176
|
+
return aOrder - bOrder;
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
getDefault() {
|
|
180
|
+
for (const plan of plans.values()) {
|
|
181
|
+
if (plan.isDefault) return plan;
|
|
182
|
+
}
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/billing/feature-gate.ts
|
|
189
|
+
import { FeatureGateError } from "@uniforge/platform-core/billing";
|
|
190
|
+
function createFeatureGateMiddleware(config) {
|
|
191
|
+
const freeFeatures = config.freeFeatures ?? [];
|
|
192
|
+
return {
|
|
193
|
+
async hasFeature(shopDomain, featureName) {
|
|
194
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
195
|
+
if (!plan) {
|
|
196
|
+
return freeFeatures.includes(featureName);
|
|
197
|
+
}
|
|
198
|
+
const planFeatures = config.planFeatures[plan.name] ?? [];
|
|
199
|
+
return planFeatures.includes(featureName) || freeFeatures.includes(featureName);
|
|
200
|
+
},
|
|
201
|
+
async requireFeature(shopDomain, featureName) {
|
|
202
|
+
const has = await this.hasFeature(shopDomain, featureName);
|
|
203
|
+
if (!has) {
|
|
204
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
205
|
+
throw new FeatureGateError(featureName, plan?.name ?? null);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
async getAvailableFeatures(shopDomain) {
|
|
209
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
210
|
+
const planFeatures = plan ? config.planFeatures[plan.name] ?? [] : [];
|
|
211
|
+
const combined = /* @__PURE__ */ new Set([...planFeatures, ...freeFeatures]);
|
|
212
|
+
return [...combined];
|
|
213
|
+
},
|
|
214
|
+
async getCurrentPlanName(shopDomain) {
|
|
215
|
+
const plan = await config.getPlanForShop(shopDomain);
|
|
216
|
+
return plan?.name ?? null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
export {
|
|
221
|
+
createBillingService,
|
|
222
|
+
createFeatureGateMiddleware,
|
|
223
|
+
createPlanRegistry
|
|
224
|
+
};
|
|
225
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/service.ts","../../src/billing/plans.ts","../../src/billing/feature-gate.ts"],"sourcesContent":["/**\n * Core billing service implementation with Prisma persistence.\n *\n * Provides subscription management and usage record tracking\n * using the PrismaClient for database operations.\n */\n\nimport { Prisma } from '@prisma/client';\nimport type { PrismaClient } from '@prisma/client';\nimport type {\n BillingService,\n} from '@uniforge/platform-core/billing';\nimport type {\n CreateSubscriptionInput,\n CreateUsageRecordInput,\n Subscription,\n SubscriptionStatus,\n UsageRecord,\n UsageRecordFilter,\n} from '@uniforge/platform-core/billing';\n\n/** Create a BillingService backed by Prisma. */\nexport function createBillingService(prisma: PrismaClient): BillingService {\n return {\n async createSubscription(input: CreateSubscriptionInput): Promise<Subscription> {\n const data: Prisma.SubscriptionUncheckedCreateInput = {\n shopDomain: input.shopDomain,\n planName: input.planName,\n status: 'PENDING',\n };\n if (input.shopifySubscriptionId) {\n data.shopifySubscriptionId = input.shopifySubscriptionId;\n }\n if (input.confirmationUrl) {\n data.confirmationUrl = input.confirmationUrl;\n }\n if (input.trialDays) {\n const trialEnd = new Date();\n trialEnd.setDate(trialEnd.getDate() + input.trialDays);\n data.trialEndsAt = trialEnd;\n }\n\n const row = await prisma.subscription.create({ data });\n return mapSubscription(row);\n },\n\n async getSubscription(id: string): Promise<Subscription | null> {\n const row = await prisma.subscription.findUnique({ where: { id } });\n return row ? mapSubscription(row) : null;\n },\n\n async getActiveSubscription(shopDomain: string): Promise<Subscription | null> {\n const row = await prisma.subscription.findFirst({\n where: { shopDomain, status: 'ACTIVE' },\n orderBy: { createdAt: 'desc' },\n });\n return row ? mapSubscription(row) : null;\n },\n\n async listSubscriptions(shopDomain: string): Promise<Subscription[]> {\n const rows = await prisma.subscription.findMany({\n where: { shopDomain },\n orderBy: { createdAt: 'desc' },\n });\n return rows.map(mapSubscription);\n },\n\n async updateSubscriptionStatus(\n id: string,\n status: SubscriptionStatus,\n ): Promise<Subscription> {\n const row = await prisma.subscription.update({\n where: { id },\n data: { status },\n });\n return mapSubscription(row);\n },\n\n async updateSubscriptionByShopifyId(\n shopifySubscriptionId: string,\n updates: Partial<Pick<Subscription, 'status' | 'currentPeriodEnd'>>,\n ): Promise<Subscription | null> {\n const data: Prisma.SubscriptionUpdateInput = {};\n if (updates.status) {\n data.status = updates.status;\n }\n if (updates.currentPeriodEnd) {\n data.currentPeriodEnd = updates.currentPeriodEnd;\n }\n\n try {\n const row = await prisma.subscription.update({\n where: { shopifySubscriptionId },\n data,\n });\n return mapSubscription(row);\n } catch (error: unknown) {\n if (\n error instanceof Prisma.PrismaClientKnownRequestError &&\n error.code === 'P2025'\n ) {\n return null;\n }\n throw error;\n }\n },\n\n async cancelSubscription(id: string): Promise<Subscription> {\n const row = await prisma.subscription.update({\n where: { id },\n data: { status: 'CANCELLED' },\n });\n return mapSubscription(row);\n },\n\n async createUsageRecord(input: CreateUsageRecordInput): Promise<UsageRecord> {\n const data: Prisma.UsageRecordUncheckedCreateInput = {\n subscriptionId: input.subscriptionId,\n description: input.description,\n amount: input.amount,\n };\n if (input.currencyCode) {\n data.currencyCode = input.currencyCode;\n }\n if (input.idempotencyKey) {\n data.idempotencyKey = input.idempotencyKey;\n }\n\n try {\n const row = await prisma.usageRecord.create({ data });\n return mapUsageRecord(row);\n } catch (error: unknown) {\n if (\n error instanceof Prisma.PrismaClientKnownRequestError &&\n error.code === 'P2002'\n ) {\n throw new Error(\n `Duplicate idempotency key: ${input.idempotencyKey}`,\n );\n }\n throw error;\n }\n },\n\n async getUsageRecords(filter: UsageRecordFilter): Promise<UsageRecord[]> {\n const where = buildUsageWhere(filter);\n const rows = await prisma.usageRecord.findMany({\n where,\n orderBy: { createdAt: 'desc' },\n });\n return rows.map(mapUsageRecord);\n },\n\n async calculateTotalUsage(filter: UsageRecordFilter): Promise<string> {\n const where = buildUsageWhere(filter);\n const result = await prisma.usageRecord.aggregate({\n where,\n _sum: { amount: true },\n });\n return result._sum.amount?.toString() ?? '0';\n },\n };\n}\n\nfunction buildUsageWhere(filter: UsageRecordFilter): Prisma.UsageRecordWhereInput {\n const where: Prisma.UsageRecordWhereInput = {\n subscriptionId: filter.subscriptionId,\n };\n if (filter.startDate || filter.endDate) {\n const createdAt: { gte?: Date; lte?: Date } = {};\n if (filter.startDate) {\n createdAt.gte = filter.startDate;\n }\n if (filter.endDate) {\n createdAt.lte = filter.endDate;\n }\n where.createdAt = createdAt;\n }\n return where;\n}\n\n/** Row shape returned by Prisma for the Subscription model. */\ninterface SubscriptionRow {\n id: string;\n shopDomain: string;\n shopifySubscriptionId: string | null;\n planName: string;\n status: string;\n currentPeriodEnd: Date | null;\n trialEndsAt: Date | null;\n confirmationUrl: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\n/** Row shape returned by Prisma for the UsageRecord model. */\ninterface UsageRecordRow {\n id: string;\n subscriptionId: string;\n description: string;\n amount: Prisma.Decimal | number;\n currencyCode: string;\n idempotencyKey: string | null;\n createdAt: Date;\n}\n\nfunction mapSubscription(row: SubscriptionRow): Subscription {\n return {\n id: row.id,\n shopDomain: row.shopDomain,\n shopifySubscriptionId: row.shopifySubscriptionId ?? null,\n planName: row.planName,\n status: row.status as SubscriptionStatus,\n currentPeriodEnd: row.currentPeriodEnd ?? null,\n trialEndsAt: row.trialEndsAt ?? null,\n confirmationUrl: row.confirmationUrl ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction mapUsageRecord(row: UsageRecordRow): UsageRecord {\n return {\n id: row.id,\n subscriptionId: row.subscriptionId,\n description: row.description,\n amount: row.amount.toString(),\n currencyCode: row.currencyCode,\n idempotencyKey: row.idempotencyKey ?? null,\n createdAt: row.createdAt,\n };\n}\n","/**\n * Billing plan registry.\n *\n * In-memory registry for managing available billing plans,\n * following the same pattern as the webhook handler registry.\n */\n\nimport type { BillingPlan, PlanRegistry } from '@uniforge/platform-core/billing';\n\n/** Create a PlanRegistry that stores plans in memory. */\nexport function createPlanRegistry(): PlanRegistry {\n const plans = new Map<string, BillingPlan>();\n\n return {\n register(plan: BillingPlan): void {\n plans.set(plan.name, plan);\n },\n\n get(name: string): BillingPlan | undefined {\n return plans.get(name);\n },\n\n list(): BillingPlan[] {\n return [...plans.values()].sort((a, b) => {\n const aOrder = a.sortOrder ?? Number.MAX_SAFE_INTEGER;\n const bOrder = b.sortOrder ?? Number.MAX_SAFE_INTEGER;\n return aOrder - bOrder;\n });\n },\n\n getDefault(): BillingPlan | undefined {\n for (const plan of plans.values()) {\n if (plan.isDefault) return plan;\n }\n return undefined;\n },\n };\n}\n","/**\n * Feature gate middleware for plan-based access control.\n *\n * Checks whether a shop's active billing plan includes a given feature.\n * Falls back to free features when no active plan exists.\n */\n\nimport type {\n FeatureGateConfig,\n FeatureGateMiddleware,\n} from '@uniforge/platform-core/billing';\nimport { FeatureGateError } from '@uniforge/platform-core/billing';\n\n/** Create a FeatureGateMiddleware from the given configuration. */\nexport function createFeatureGateMiddleware(\n config: FeatureGateConfig,\n): FeatureGateMiddleware {\n const freeFeatures = config.freeFeatures ?? [];\n\n return {\n async hasFeature(shopDomain: string, featureName: string): Promise<boolean> {\n const plan = await config.getPlanForShop(shopDomain);\n if (!plan) {\n return freeFeatures.includes(featureName);\n }\n const planFeatures = config.planFeatures[plan.name] ?? [];\n return planFeatures.includes(featureName) || freeFeatures.includes(featureName);\n },\n\n async requireFeature(shopDomain: string, featureName: string): Promise<void> {\n const has = await this.hasFeature(shopDomain, featureName);\n if (!has) {\n const plan = await config.getPlanForShop(shopDomain);\n throw new FeatureGateError(featureName, plan?.name ?? null);\n }\n },\n\n async getAvailableFeatures(shopDomain: string): Promise<string[]> {\n const plan = await config.getPlanForShop(shopDomain);\n const planFeatures = plan ? (config.planFeatures[plan.name] ?? []) : [];\n const combined = new Set([...planFeatures, ...freeFeatures]);\n return [...combined];\n },\n\n async getCurrentPlanName(shopDomain: string): Promise<string | null> {\n const plan = await config.getPlanForShop(shopDomain);\n return plan?.name ?? null;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { U as UniforgeConfig } from '../schema-CM7mHj_H.cjs';
|
|
2
|
+
export { d as defaultConfig } from '../schema-CM7mHj_H.cjs';
|
|
3
|
+
|
|
4
|
+
declare function loadConfig(configPath?: string): Promise<UniforgeConfig>;
|
|
5
|
+
|
|
6
|
+
declare class ConfigValidationError extends Error {
|
|
7
|
+
readonly errors: string[];
|
|
8
|
+
constructor(errors: string[]);
|
|
9
|
+
}
|
|
10
|
+
declare function validateConfig(config: unknown): UniforgeConfig;
|
|
11
|
+
|
|
12
|
+
export { ConfigValidationError, UniforgeConfig, loadConfig, validateConfig };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { U as UniforgeConfig } from '../schema-CM7mHj_H.js';
|
|
2
|
+
export { d as defaultConfig } from '../schema-CM7mHj_H.js';
|
|
3
|
+
|
|
4
|
+
declare function loadConfig(configPath?: string): Promise<UniforgeConfig>;
|
|
5
|
+
|
|
6
|
+
declare class ConfigValidationError extends Error {
|
|
7
|
+
readonly errors: string[];
|
|
8
|
+
constructor(errors: string[]);
|
|
9
|
+
}
|
|
10
|
+
declare function validateConfig(config: unknown): UniforgeConfig;
|
|
11
|
+
|
|
12
|
+
export { ConfigValidationError, UniforgeConfig, loadConfig, validateConfig };
|