@qazuor/qzpay-core 1.0.0 → 1.0.1
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/README.md +69 -0
- package/dist/index.cjs +306 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -4
- package/dist/index.d.ts +96 -4
- package/dist/index.js +306 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ pnpm add @qazuor/qzpay-core
|
|
|
16
16
|
- **Metrics Service**: MRR, churn, and revenue calculations
|
|
17
17
|
- **Health Service**: System health monitoring
|
|
18
18
|
- **Logger**: Structured logging with customizable providers
|
|
19
|
+
- **Input Validation**: Zod-based validation for all create operations
|
|
20
|
+
- **Promo Code Validation**: Full validation with plan applicability and usage limits
|
|
19
21
|
- **Utilities**: Date, money, validation, and hash helpers
|
|
20
22
|
- **Runtime Agnostic**: Works in Node.js, Bun, Deno, and Edge runtimes (no direct `process.env` access)
|
|
21
23
|
|
|
@@ -240,6 +242,73 @@ isValidAmount(100); // true
|
|
|
240
242
|
isValidAmount(-50); // false
|
|
241
243
|
```
|
|
242
244
|
|
|
245
|
+
### Input Validation
|
|
246
|
+
|
|
247
|
+
All create operations now include comprehensive Zod validation:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Customer creation with validation
|
|
251
|
+
try {
|
|
252
|
+
const customer = await billing.customers.create({
|
|
253
|
+
email: 'invalid-email', // Will throw validation error
|
|
254
|
+
name: 'John Doe'
|
|
255
|
+
});
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// error.code === 'VALIDATION_ERROR'
|
|
258
|
+
// error.details contains validation failure info
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Payment with validation
|
|
262
|
+
await billing.payments.process({
|
|
263
|
+
customerId: 'cus_123',
|
|
264
|
+
amount: -100, // Will throw validation error (negative amount)
|
|
265
|
+
currency: 'INVALID' // Will throw validation error (invalid currency)
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Invoice creation with validation
|
|
269
|
+
await billing.invoices.create({
|
|
270
|
+
customerId: 'cus_123',
|
|
271
|
+
items: [] // Will throw validation error (empty items array)
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Promo Code Validation
|
|
276
|
+
|
|
277
|
+
Promo codes are validated against multiple criteria:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// The system automatically validates:
|
|
281
|
+
// 1. Plan applicability
|
|
282
|
+
const promoCode = await billing.promoCodes.create({
|
|
283
|
+
code: 'SUMMER2024',
|
|
284
|
+
discountType: 'percentage',
|
|
285
|
+
discountValue: 20,
|
|
286
|
+
applicablePlans: ['plan_premium', 'plan_enterprise'] // Only works for these plans
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 2. Per-customer usage limits
|
|
290
|
+
await billing.promoCodes.validate({
|
|
291
|
+
code: 'SUMMER2024',
|
|
292
|
+
customerId: 'cus_123',
|
|
293
|
+
planId: 'plan_basic' // Will fail if not in applicablePlans
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// 3. Date ranges
|
|
297
|
+
await billing.promoCodes.create({
|
|
298
|
+
code: 'NEWYEAR2024',
|
|
299
|
+
validFrom: new Date('2024-01-01'),
|
|
300
|
+
validTo: new Date('2024-01-31') // Only valid in January
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 4. Max uses and active status
|
|
304
|
+
await billing.promoCodes.create({
|
|
305
|
+
code: 'LIMITED',
|
|
306
|
+
maxUses: 100, // Can only be used 100 times total
|
|
307
|
+
maxUsesPerCustomer: 1, // Each customer can use it once
|
|
308
|
+
active: true // Must be active
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
243
312
|
## Types
|
|
244
313
|
|
|
245
314
|
### Core Types
|
package/dist/index.cjs
CHANGED
|
@@ -1280,6 +1280,108 @@ var noopLogger = {
|
|
|
1280
1280
|
}
|
|
1281
1281
|
};
|
|
1282
1282
|
|
|
1283
|
+
// src/utils/validation.utils.ts
|
|
1284
|
+
function qzpayIsValidEmail(email) {
|
|
1285
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1286
|
+
return emailRegex.test(email);
|
|
1287
|
+
}
|
|
1288
|
+
function qzpayIsValidCurrency(currency) {
|
|
1289
|
+
return QZPAY_CURRENCY_VALUES.includes(currency.toUpperCase());
|
|
1290
|
+
}
|
|
1291
|
+
function qzpayIsPositiveInteger(value) {
|
|
1292
|
+
return Number.isInteger(value) && value > 0;
|
|
1293
|
+
}
|
|
1294
|
+
function qzpayIsNonNegativeInteger(value) {
|
|
1295
|
+
return Number.isInteger(value) && value >= 0;
|
|
1296
|
+
}
|
|
1297
|
+
function qzpayIsValidPercentage(value) {
|
|
1298
|
+
return value >= 0 && value <= 100;
|
|
1299
|
+
}
|
|
1300
|
+
function qzpayIsValidUuid(value) {
|
|
1301
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1302
|
+
return uuidRegex.test(value);
|
|
1303
|
+
}
|
|
1304
|
+
function qzpayIsRequiredString(value) {
|
|
1305
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1306
|
+
}
|
|
1307
|
+
function qzpayValidateRequired(obj, requiredFields) {
|
|
1308
|
+
const errors = [];
|
|
1309
|
+
for (const field of requiredFields) {
|
|
1310
|
+
const value = obj[field];
|
|
1311
|
+
if (value === void 0 || value === null || value === "") {
|
|
1312
|
+
errors.push(`${String(field)} is required`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return {
|
|
1316
|
+
valid: errors.length === 0,
|
|
1317
|
+
errors
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function qzpayAssert(condition, message) {
|
|
1321
|
+
if (!condition) {
|
|
1322
|
+
throw new Error(message);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function qzpayAssertDefined(value, name) {
|
|
1326
|
+
if (value === null || value === void 0) {
|
|
1327
|
+
throw new Error(`${name} is required`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function qzpayCreateValidator(obj) {
|
|
1331
|
+
return new QZPayValidator(obj);
|
|
1332
|
+
}
|
|
1333
|
+
var QZPayValidator = class {
|
|
1334
|
+
constructor(obj) {
|
|
1335
|
+
this.obj = obj;
|
|
1336
|
+
}
|
|
1337
|
+
errors = [];
|
|
1338
|
+
required(field, message) {
|
|
1339
|
+
const value = this.obj[field];
|
|
1340
|
+
if (value === void 0 || value === null || value === "") {
|
|
1341
|
+
this.errors.push(message ?? `${String(field)} is required`);
|
|
1342
|
+
}
|
|
1343
|
+
return this;
|
|
1344
|
+
}
|
|
1345
|
+
email(field, message) {
|
|
1346
|
+
const value = this.obj[field];
|
|
1347
|
+
if (typeof value === "string" && !qzpayIsValidEmail(value)) {
|
|
1348
|
+
this.errors.push(message ?? `${String(field)} must be a valid email`);
|
|
1349
|
+
}
|
|
1350
|
+
return this;
|
|
1351
|
+
}
|
|
1352
|
+
positiveInteger(field, message) {
|
|
1353
|
+
const value = this.obj[field];
|
|
1354
|
+
if (typeof value === "number" && !qzpayIsPositiveInteger(value)) {
|
|
1355
|
+
this.errors.push(message ?? `${String(field)} must be a positive integer`);
|
|
1356
|
+
}
|
|
1357
|
+
return this;
|
|
1358
|
+
}
|
|
1359
|
+
currency(field, message) {
|
|
1360
|
+
const value = this.obj[field];
|
|
1361
|
+
if (typeof value === "string" && !qzpayIsValidCurrency(value)) {
|
|
1362
|
+
this.errors.push(message ?? `${String(field)} must be a valid currency`);
|
|
1363
|
+
}
|
|
1364
|
+
return this;
|
|
1365
|
+
}
|
|
1366
|
+
custom(condition, message) {
|
|
1367
|
+
if (!condition) {
|
|
1368
|
+
this.errors.push(message);
|
|
1369
|
+
}
|
|
1370
|
+
return this;
|
|
1371
|
+
}
|
|
1372
|
+
validate() {
|
|
1373
|
+
return {
|
|
1374
|
+
valid: this.errors.length === 0,
|
|
1375
|
+
errors: [...this.errors]
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
assertValid() {
|
|
1379
|
+
if (this.errors.length > 0) {
|
|
1380
|
+
throw new Error(`Validation failed: ${this.errors.join(", ")}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1283
1385
|
// src/billing.ts
|
|
1284
1386
|
var QZPayBillingImpl = class {
|
|
1285
1387
|
storage;
|
|
@@ -1307,40 +1409,128 @@ var QZPayBillingImpl = class {
|
|
|
1307
1409
|
this.logger.info("QZPayBilling initialized", { livemode: this.livemode });
|
|
1308
1410
|
}
|
|
1309
1411
|
get customers() {
|
|
1412
|
+
const storage = this.storage;
|
|
1413
|
+
const emitter = this.emitter;
|
|
1414
|
+
const paymentAdapter = this.paymentAdapter;
|
|
1310
1415
|
return {
|
|
1311
1416
|
create: async (input) => {
|
|
1312
|
-
|
|
1313
|
-
await
|
|
1417
|
+
qzpayCreateValidator(input).required("email", "Email is required").email("email", "Invalid email format").required("externalId", "External ID is required").custom(!input.name || typeof input.name === "string" && input.name.trim().length > 0, "Name cannot be empty string").assertValid();
|
|
1418
|
+
const customer = await storage.customers.create(input);
|
|
1419
|
+
if (paymentAdapter) {
|
|
1420
|
+
try {
|
|
1421
|
+
const providerInput = {
|
|
1422
|
+
email: input.email,
|
|
1423
|
+
externalId: input.externalId
|
|
1424
|
+
};
|
|
1425
|
+
if (input.name !== void 0) providerInput.name = input.name;
|
|
1426
|
+
if (input.metadata) providerInput.metadata = input.metadata;
|
|
1427
|
+
const providerCustomerId = await paymentAdapter.customers.create(providerInput);
|
|
1428
|
+
const updated = await storage.customers.update(customer.id, {
|
|
1429
|
+
providerCustomerIds: {
|
|
1430
|
+
...customer.providerCustomerIds,
|
|
1431
|
+
[paymentAdapter.provider]: providerCustomerId
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
await emitter.emit("customer.created", updated ?? customer);
|
|
1435
|
+
return updated ?? customer;
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
this.logger.error("Failed to create customer in provider", {
|
|
1438
|
+
provider: paymentAdapter.provider,
|
|
1439
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1440
|
+
});
|
|
1441
|
+
await emitter.emit("customer.created", customer);
|
|
1442
|
+
return customer;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
await emitter.emit("customer.created", customer);
|
|
1314
1446
|
return customer;
|
|
1315
1447
|
},
|
|
1316
|
-
get: (id) =>
|
|
1317
|
-
getByExternalId: (externalId) =>
|
|
1448
|
+
get: (id) => storage.customers.findById(id),
|
|
1449
|
+
getByExternalId: (externalId) => storage.customers.findByExternalId(externalId),
|
|
1318
1450
|
update: async (id, input) => {
|
|
1319
|
-
const customer = await
|
|
1451
|
+
const customer = await storage.customers.update(id, input);
|
|
1320
1452
|
if (customer) {
|
|
1321
|
-
await
|
|
1453
|
+
await emitter.emit("customer.updated", customer);
|
|
1322
1454
|
}
|
|
1323
1455
|
return customer;
|
|
1324
1456
|
},
|
|
1325
1457
|
delete: async (id) => {
|
|
1326
|
-
await
|
|
1327
|
-
const customer = await
|
|
1458
|
+
await storage.customers.delete(id);
|
|
1459
|
+
const customer = await storage.customers.findById(id);
|
|
1328
1460
|
if (customer) {
|
|
1329
|
-
await
|
|
1461
|
+
await emitter.emit("customer.deleted", customer);
|
|
1330
1462
|
}
|
|
1331
1463
|
},
|
|
1332
|
-
list: (options) =>
|
|
1464
|
+
list: (options) => storage.customers.list(options),
|
|
1333
1465
|
syncUser: async (input) => {
|
|
1334
|
-
const existing = await
|
|
1466
|
+
const existing = await storage.customers.findByExternalId(input.externalId ?? "");
|
|
1335
1467
|
if (existing) {
|
|
1336
|
-
|
|
1468
|
+
if (paymentAdapter && !existing.providerCustomerIds[paymentAdapter.provider]) {
|
|
1469
|
+
try {
|
|
1470
|
+
const email = input.email ?? existing.email;
|
|
1471
|
+
const name = input.name !== void 0 ? input.name : existing.name;
|
|
1472
|
+
const metadata = input.metadata ?? existing.metadata;
|
|
1473
|
+
const externalId = input.externalId ?? existing.externalId;
|
|
1474
|
+
const providerInput = {
|
|
1475
|
+
email,
|
|
1476
|
+
externalId
|
|
1477
|
+
};
|
|
1478
|
+
if (name !== void 0) providerInput.name = name;
|
|
1479
|
+
if (metadata) providerInput.metadata = metadata;
|
|
1480
|
+
const providerCustomerId = await paymentAdapter.customers.create(providerInput);
|
|
1481
|
+
const updated2 = await storage.customers.update(existing.id, {
|
|
1482
|
+
...input,
|
|
1483
|
+
providerCustomerIds: {
|
|
1484
|
+
...existing.providerCustomerIds,
|
|
1485
|
+
[paymentAdapter.provider]: providerCustomerId
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
if (updated2) {
|
|
1489
|
+
await emitter.emit("customer.updated", updated2);
|
|
1490
|
+
}
|
|
1491
|
+
return updated2 ?? existing;
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
this.logger.error("Failed to sync existing customer with provider", {
|
|
1494
|
+
provider: paymentAdapter.provider,
|
|
1495
|
+
customerId: existing.id,
|
|
1496
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
const updated = await storage.customers.update(existing.id, input);
|
|
1337
1501
|
if (updated) {
|
|
1338
|
-
await
|
|
1502
|
+
await emitter.emit("customer.updated", updated);
|
|
1339
1503
|
}
|
|
1340
1504
|
return updated ?? existing;
|
|
1341
1505
|
}
|
|
1342
|
-
const customer = await
|
|
1343
|
-
|
|
1506
|
+
const customer = await storage.customers.create(input);
|
|
1507
|
+
if (paymentAdapter) {
|
|
1508
|
+
try {
|
|
1509
|
+
const providerInput = {
|
|
1510
|
+
email: input.email,
|
|
1511
|
+
externalId: input.externalId
|
|
1512
|
+
};
|
|
1513
|
+
if (input.name !== void 0) providerInput.name = input.name;
|
|
1514
|
+
if (input.metadata) providerInput.metadata = input.metadata;
|
|
1515
|
+
const providerCustomerId = await paymentAdapter.customers.create(providerInput);
|
|
1516
|
+
const updated = await storage.customers.update(customer.id, {
|
|
1517
|
+
providerCustomerIds: {
|
|
1518
|
+
...customer.providerCustomerIds,
|
|
1519
|
+
[paymentAdapter.provider]: providerCustomerId
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
await emitter.emit("customer.created", updated ?? customer);
|
|
1523
|
+
return updated ?? customer;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
this.logger.error("Failed to sync new customer with provider", {
|
|
1526
|
+
provider: paymentAdapter.provider,
|
|
1527
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1528
|
+
});
|
|
1529
|
+
await emitter.emit("customer.created", customer);
|
|
1530
|
+
return customer;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
await emitter.emit("customer.created", customer);
|
|
1344
1534
|
return customer;
|
|
1345
1535
|
}
|
|
1346
1536
|
};
|
|
@@ -1416,13 +1606,29 @@ var QZPayBillingImpl = class {
|
|
|
1416
1606
|
if (!currentSubscription) {
|
|
1417
1607
|
throw new Error(`Subscription ${id} not found`);
|
|
1418
1608
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1609
|
+
let currentPlan = planMap.get(currentSubscription.planId);
|
|
1610
|
+
if (!currentPlan) {
|
|
1611
|
+
currentPlan = await storage.plans.findById(currentSubscription.planId) ?? void 0;
|
|
1612
|
+
}
|
|
1613
|
+
let currentPrice = currentPlan?.prices[0] ?? null;
|
|
1614
|
+
if (currentPlan && (!currentPlan.prices || currentPlan.prices.length === 0)) {
|
|
1615
|
+
const storagePrices = await storage.prices.findByPlanId(currentSubscription.planId);
|
|
1616
|
+
if (storagePrices.length > 0) {
|
|
1617
|
+
currentPrice = storagePrices[0] ?? null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
let newPlan = planMap.get(options.newPlanId);
|
|
1621
|
+
if (!newPlan) {
|
|
1622
|
+
newPlan = await storage.plans.findById(options.newPlanId) ?? void 0;
|
|
1623
|
+
}
|
|
1422
1624
|
if (!newPlan) {
|
|
1423
1625
|
throw new Error(`Plan ${options.newPlanId} not found`);
|
|
1424
1626
|
}
|
|
1425
|
-
|
|
1627
|
+
let newPlanPrices = newPlan.prices || [];
|
|
1628
|
+
if (newPlanPrices.length === 0) {
|
|
1629
|
+
newPlanPrices = await storage.prices.findByPlanId(options.newPlanId);
|
|
1630
|
+
}
|
|
1631
|
+
const newPrice = options.newPriceId ? newPlanPrices.find((p) => p.id === options.newPriceId) : newPlanPrices[0];
|
|
1426
1632
|
if (!newPrice) {
|
|
1427
1633
|
throw new Error(`Price not found for plan ${options.newPlanId}`);
|
|
1428
1634
|
}
|
|
@@ -1476,6 +1682,7 @@ var QZPayBillingImpl = class {
|
|
|
1476
1682
|
return {
|
|
1477
1683
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex payment processing with provider integration and error handling
|
|
1478
1684
|
process: async (input) => {
|
|
1685
|
+
qzpayCreateValidator(input).required("amount", "Amount is required").custom(typeof input.amount === "number" && input.amount > 0, "Amount must be greater than 0").required("currency", "Currency is required").currency("currency", "Invalid currency code").assertValid();
|
|
1479
1686
|
const paymentId = crypto.randomUUID();
|
|
1480
1687
|
const now = /* @__PURE__ */ new Date();
|
|
1481
1688
|
const payment = await storage.payments.create({
|
|
@@ -1511,6 +1718,15 @@ var QZPayBillingImpl = class {
|
|
|
1511
1718
|
if (input.subscriptionId !== void 0) paymentInput.subscriptionId = input.subscriptionId;
|
|
1512
1719
|
if (input.invoiceId !== void 0) paymentInput.invoiceId = input.invoiceId;
|
|
1513
1720
|
if (input.metadata !== void 0) paymentInput.metadata = input.metadata;
|
|
1721
|
+
if (input.token !== void 0) paymentInput.token = input.token;
|
|
1722
|
+
if (input.cardId !== void 0) paymentInput.cardId = input.cardId;
|
|
1723
|
+
if (input.installments !== void 0) paymentInput.installments = input.installments;
|
|
1724
|
+
if (input.payerEmail) {
|
|
1725
|
+
paymentInput.payerEmail = input.payerEmail;
|
|
1726
|
+
} else if (customer?.email) {
|
|
1727
|
+
paymentInput.payerEmail = customer.email;
|
|
1728
|
+
}
|
|
1729
|
+
if (input.payerIdentification) paymentInput.payerIdentification = input.payerIdentification;
|
|
1514
1730
|
const result = await paymentAdapter.payments.create(providerCustomerId, paymentInput);
|
|
1515
1731
|
const updated = await storage.payments.update(paymentId, {
|
|
1516
1732
|
status: "succeeded",
|
|
@@ -1518,8 +1734,20 @@ var QZPayBillingImpl = class {
|
|
|
1518
1734
|
});
|
|
1519
1735
|
await emitter.emit("payment.succeeded", updated);
|
|
1520
1736
|
return updated;
|
|
1521
|
-
} catch {
|
|
1522
|
-
const
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown payment error";
|
|
1739
|
+
this.logger.error("Payment processing failed", {
|
|
1740
|
+
provider: paymentAdapter.provider,
|
|
1741
|
+
customerId: input.customerId,
|
|
1742
|
+
amount: input.amount,
|
|
1743
|
+
currency: input.currency,
|
|
1744
|
+
error: errorMessage
|
|
1745
|
+
});
|
|
1746
|
+
const failed = await storage.payments.update(paymentId, {
|
|
1747
|
+
status: "failed",
|
|
1748
|
+
failureMessage: errorMessage,
|
|
1749
|
+
failureCode: "payment_failed"
|
|
1750
|
+
});
|
|
1523
1751
|
await emitter.emit("payment.failed", failed);
|
|
1524
1752
|
return failed;
|
|
1525
1753
|
}
|
|
@@ -1528,6 +1756,39 @@ var QZPayBillingImpl = class {
|
|
|
1528
1756
|
},
|
|
1529
1757
|
get: (id) => storage.payments.findById(id),
|
|
1530
1758
|
getByCustomerId: (customerId) => storage.payments.findByCustomerId(customerId),
|
|
1759
|
+
/**
|
|
1760
|
+
* Record an external payment (already processed by a payment provider)
|
|
1761
|
+
* Use this when the payment was processed by an external system (e.g., backend API)
|
|
1762
|
+
* and you need to create a local record for tracking purposes.
|
|
1763
|
+
*/
|
|
1764
|
+
record: async (input) => {
|
|
1765
|
+
const now = /* @__PURE__ */ new Date();
|
|
1766
|
+
const providerPaymentIds = {};
|
|
1767
|
+
if (input.provider && input.providerPaymentId) {
|
|
1768
|
+
providerPaymentIds[input.provider] = input.providerPaymentId;
|
|
1769
|
+
}
|
|
1770
|
+
const payment = await storage.payments.create({
|
|
1771
|
+
id: input.id,
|
|
1772
|
+
customerId: input.customerId,
|
|
1773
|
+
amount: input.amount,
|
|
1774
|
+
currency: input.currency,
|
|
1775
|
+
status: input.status,
|
|
1776
|
+
invoiceId: input.invoiceId ?? null,
|
|
1777
|
+
subscriptionId: input.subscriptionId ?? null,
|
|
1778
|
+
paymentMethodId: null,
|
|
1779
|
+
failureCode: null,
|
|
1780
|
+
failureMessage: null,
|
|
1781
|
+
metadata: input.metadata ?? {},
|
|
1782
|
+
livemode: this.livemode,
|
|
1783
|
+
createdAt: now,
|
|
1784
|
+
updatedAt: now,
|
|
1785
|
+
providerPaymentIds
|
|
1786
|
+
});
|
|
1787
|
+
if (input.status === "succeeded") {
|
|
1788
|
+
await emitter.emit("payment.succeeded", payment);
|
|
1789
|
+
}
|
|
1790
|
+
return payment;
|
|
1791
|
+
},
|
|
1531
1792
|
refund: async (input) => {
|
|
1532
1793
|
const payment = await storage.payments.findById(input.paymentId);
|
|
1533
1794
|
if (!payment) {
|
|
@@ -1549,6 +1810,10 @@ var QZPayBillingImpl = class {
|
|
|
1549
1810
|
const emitter = this.emitter;
|
|
1550
1811
|
return {
|
|
1551
1812
|
create: async (input) => {
|
|
1813
|
+
qzpayCreateValidator(input).required("customerId", "Customer ID is required").required("lines", "Invoice lines are required").custom(Array.isArray(input.lines) && input.lines.length > 0, "Invoice must have at least one line item").assertValid();
|
|
1814
|
+
for (const line of input.lines) {
|
|
1815
|
+
qzpayCreateValidator(line).custom(typeof line.quantity === "number" && line.quantity > 0, "Line quantity must be greater than 0").custom(typeof line.unitAmount === "number" && line.unitAmount >= 0, "Line unit amount must be non-negative").assertValid();
|
|
1816
|
+
}
|
|
1552
1817
|
const invoice = await storage.invoices.create({
|
|
1553
1818
|
id: crypto.randomUUID(),
|
|
1554
1819
|
customerId: input.customerId,
|
|
@@ -1601,7 +1866,7 @@ var QZPayBillingImpl = class {
|
|
|
1601
1866
|
get promoCodes() {
|
|
1602
1867
|
const storage = this.storage;
|
|
1603
1868
|
return {
|
|
1604
|
-
validate: async (code,
|
|
1869
|
+
validate: async (code, customerId, planId) => {
|
|
1605
1870
|
const promoCode = await storage.promoCodes.findByCode(code);
|
|
1606
1871
|
if (!promoCode) {
|
|
1607
1872
|
return { valid: false, promoCode: null, error: "Promo code not found" };
|
|
@@ -1609,12 +1874,26 @@ var QZPayBillingImpl = class {
|
|
|
1609
1874
|
if (!promoCode.active) {
|
|
1610
1875
|
return { valid: false, promoCode, error: "Promo code is not active" };
|
|
1611
1876
|
}
|
|
1612
|
-
|
|
1877
|
+
const now = /* @__PURE__ */ new Date();
|
|
1878
|
+
if (promoCode.validFrom && now < promoCode.validFrom) {
|
|
1879
|
+
return { valid: false, promoCode, error: "Promo code is not yet valid" };
|
|
1880
|
+
}
|
|
1881
|
+
if (promoCode.validUntil && now > promoCode.validUntil) {
|
|
1613
1882
|
return { valid: false, promoCode, error: "Promo code has expired" };
|
|
1614
1883
|
}
|
|
1615
1884
|
if (promoCode.maxRedemptions && promoCode.currentRedemptions >= promoCode.maxRedemptions) {
|
|
1616
1885
|
return { valid: false, promoCode, error: "Promo code has reached max redemptions" };
|
|
1617
1886
|
}
|
|
1887
|
+
if (planId && promoCode.applicablePlanIds.length > 0 && !promoCode.applicablePlanIds.includes(planId)) {
|
|
1888
|
+
return { valid: false, promoCode, error: "Promo code is not applicable to this plan" };
|
|
1889
|
+
}
|
|
1890
|
+
if (customerId && promoCode.maxRedemptionsPerCustomer !== null && promoCode.maxRedemptionsPerCustomer > 0) {
|
|
1891
|
+
const customerSubscriptions = await storage.subscriptions.findByCustomerId(customerId);
|
|
1892
|
+
const redemptionCount = customerSubscriptions.filter((sub) => sub.promoCodeId === promoCode.id).length;
|
|
1893
|
+
if (redemptionCount >= promoCode.maxRedemptionsPerCustomer) {
|
|
1894
|
+
return { valid: false, promoCode, error: "You have reached the maximum redemption limit for this promo code" };
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1618
1897
|
const result = {
|
|
1619
1898
|
valid: true,
|
|
1620
1899
|
promoCode
|
|
@@ -1767,6 +2046,10 @@ var QZPayBillingImpl = class {
|
|
|
1767
2046
|
const emitter = this.emitter;
|
|
1768
2047
|
return {
|
|
1769
2048
|
create: async (input) => {
|
|
2049
|
+
qzpayCreateValidator(input).required("name", "Add-on name is required").required("unitAmount", "Unit amount is required").custom(typeof input.unitAmount === "number" && input.unitAmount > 0, "Unit amount must be greater than 0").required("currency", "Currency is required").currency("currency", "Invalid currency code").custom(
|
|
2050
|
+
!input.billingIntervalCount || typeof input.billingIntervalCount === "number" && input.billingIntervalCount > 0,
|
|
2051
|
+
"Billing interval count must be greater than 0"
|
|
2052
|
+
).assertValid();
|
|
1770
2053
|
const addon = await storage.addons.create({
|
|
1771
2054
|
id: input.id ?? crypto.randomUUID(),
|
|
1772
2055
|
...input
|
|
@@ -6768,108 +7051,6 @@ function createHealthService(config) {
|
|
|
6768
7051
|
return new QZPayHealthService(config);
|
|
6769
7052
|
}
|
|
6770
7053
|
|
|
6771
|
-
// src/utils/validation.utils.ts
|
|
6772
|
-
function qzpayIsValidEmail(email) {
|
|
6773
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6774
|
-
return emailRegex.test(email);
|
|
6775
|
-
}
|
|
6776
|
-
function qzpayIsValidCurrency(currency) {
|
|
6777
|
-
return QZPAY_CURRENCY_VALUES.includes(currency);
|
|
6778
|
-
}
|
|
6779
|
-
function qzpayIsPositiveInteger(value) {
|
|
6780
|
-
return Number.isInteger(value) && value > 0;
|
|
6781
|
-
}
|
|
6782
|
-
function qzpayIsNonNegativeInteger(value) {
|
|
6783
|
-
return Number.isInteger(value) && value >= 0;
|
|
6784
|
-
}
|
|
6785
|
-
function qzpayIsValidPercentage(value) {
|
|
6786
|
-
return value >= 0 && value <= 100;
|
|
6787
|
-
}
|
|
6788
|
-
function qzpayIsValidUuid(value) {
|
|
6789
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6790
|
-
return uuidRegex.test(value);
|
|
6791
|
-
}
|
|
6792
|
-
function qzpayIsRequiredString(value) {
|
|
6793
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
6794
|
-
}
|
|
6795
|
-
function qzpayValidateRequired(obj, requiredFields) {
|
|
6796
|
-
const errors = [];
|
|
6797
|
-
for (const field of requiredFields) {
|
|
6798
|
-
const value = obj[field];
|
|
6799
|
-
if (value === void 0 || value === null || value === "") {
|
|
6800
|
-
errors.push(`${String(field)} is required`);
|
|
6801
|
-
}
|
|
6802
|
-
}
|
|
6803
|
-
return {
|
|
6804
|
-
valid: errors.length === 0,
|
|
6805
|
-
errors
|
|
6806
|
-
};
|
|
6807
|
-
}
|
|
6808
|
-
function qzpayAssert(condition, message) {
|
|
6809
|
-
if (!condition) {
|
|
6810
|
-
throw new Error(message);
|
|
6811
|
-
}
|
|
6812
|
-
}
|
|
6813
|
-
function qzpayAssertDefined(value, name) {
|
|
6814
|
-
if (value === null || value === void 0) {
|
|
6815
|
-
throw new Error(`${name} is required`);
|
|
6816
|
-
}
|
|
6817
|
-
}
|
|
6818
|
-
function qzpayCreateValidator(obj) {
|
|
6819
|
-
return new QZPayValidator(obj);
|
|
6820
|
-
}
|
|
6821
|
-
var QZPayValidator = class {
|
|
6822
|
-
constructor(obj) {
|
|
6823
|
-
this.obj = obj;
|
|
6824
|
-
}
|
|
6825
|
-
errors = [];
|
|
6826
|
-
required(field, message) {
|
|
6827
|
-
const value = this.obj[field];
|
|
6828
|
-
if (value === void 0 || value === null || value === "") {
|
|
6829
|
-
this.errors.push(message ?? `${String(field)} is required`);
|
|
6830
|
-
}
|
|
6831
|
-
return this;
|
|
6832
|
-
}
|
|
6833
|
-
email(field, message) {
|
|
6834
|
-
const value = this.obj[field];
|
|
6835
|
-
if (typeof value === "string" && !qzpayIsValidEmail(value)) {
|
|
6836
|
-
this.errors.push(message ?? `${String(field)} must be a valid email`);
|
|
6837
|
-
}
|
|
6838
|
-
return this;
|
|
6839
|
-
}
|
|
6840
|
-
positiveInteger(field, message) {
|
|
6841
|
-
const value = this.obj[field];
|
|
6842
|
-
if (typeof value === "number" && !qzpayIsPositiveInteger(value)) {
|
|
6843
|
-
this.errors.push(message ?? `${String(field)} must be a positive integer`);
|
|
6844
|
-
}
|
|
6845
|
-
return this;
|
|
6846
|
-
}
|
|
6847
|
-
currency(field, message) {
|
|
6848
|
-
const value = this.obj[field];
|
|
6849
|
-
if (typeof value === "string" && !qzpayIsValidCurrency(value)) {
|
|
6850
|
-
this.errors.push(message ?? `${String(field)} must be a valid currency`);
|
|
6851
|
-
}
|
|
6852
|
-
return this;
|
|
6853
|
-
}
|
|
6854
|
-
custom(condition, message) {
|
|
6855
|
-
if (!condition) {
|
|
6856
|
-
this.errors.push(message);
|
|
6857
|
-
}
|
|
6858
|
-
return this;
|
|
6859
|
-
}
|
|
6860
|
-
validate() {
|
|
6861
|
-
return {
|
|
6862
|
-
valid: this.errors.length === 0,
|
|
6863
|
-
errors: [...this.errors]
|
|
6864
|
-
};
|
|
6865
|
-
}
|
|
6866
|
-
assertValid() {
|
|
6867
|
-
if (this.errors.length > 0) {
|
|
6868
|
-
throw new Error(`Validation failed: ${this.errors.join(", ")}`);
|
|
6869
|
-
}
|
|
6870
|
-
}
|
|
6871
|
-
};
|
|
6872
|
-
|
|
6873
7054
|
exports.QZPAY_BILLING_EVENT = QZPAY_BILLING_EVENT;
|
|
6874
7055
|
exports.QZPAY_BILLING_EVENT_VALUES = QZPAY_BILLING_EVENT_VALUES;
|
|
6875
7056
|
exports.QZPAY_BILLING_INTERVAL = QZPAY_BILLING_INTERVAL;
|