payment-kit 1.29.1 → 1.29.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/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +81 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +50 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +259 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +214 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +17 -12
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +12 -4
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +667 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +66 -22
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +236 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +10 -56
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -1,22 +1,27 @@
|
|
|
1
|
+
// Phase 3 (express→hono) — hono fork of routes/prices.ts. Sub-app with
|
|
2
|
+
// routes relative to /api/prices (mounted via mountResourceGroup). The
|
|
3
|
+
// business logic is unchanged; only the express plumbing becomes hono:
|
|
4
|
+
// req.body → c.get('sanitizedBody'); res.status(n).json(x) → c.json(x, n).
|
|
1
5
|
import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
2
|
-
import {
|
|
6
|
+
import { Hono } from 'hono';
|
|
3
7
|
import Joi from 'joi';
|
|
4
8
|
import pick from 'lodash/pick';
|
|
5
9
|
import type { WhereOptions } from 'sequelize';
|
|
6
10
|
|
|
7
|
-
import { createListParamSchema, getOrder, getWhereFromQuery, MetadataSchema } from '
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
import { createListParamSchema, getOrder, getWhereFromQuery, MetadataSchema } from '../../libs/api';
|
|
12
|
+
import { allowChangeLockedPrice } from '../../libs/env';
|
|
13
|
+
import { getExchangeRateService } from '../../libs/exchange-rate';
|
|
14
|
+
import { getExchangeRateSymbol, hasTokenAddress } from '../../libs/exchange-rate/token-address-mapping';
|
|
15
|
+
import logger from '../../libs/logger';
|
|
16
|
+
import { authenticate } from '../../middlewares/hono/security';
|
|
17
|
+
import { canUpsell } from '../../libs/session';
|
|
18
|
+
import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
19
|
+
import { Price } from '../../store/models/price';
|
|
20
|
+
import { Product } from '../../store/models/product';
|
|
21
|
+
import { checkCurrencySupportRecurring } from '../../libs/product';
|
|
22
|
+
import { ChainType, Meter, PaymentMethod } from '../../store/models';
|
|
23
|
+
|
|
24
|
+
const app = new Hono();
|
|
20
25
|
|
|
21
26
|
const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
|
|
22
27
|
|
|
@@ -147,139 +152,84 @@ const paginationSchema = createListParamSchema<{
|
|
|
147
152
|
product_id: Joi.string().empty(''),
|
|
148
153
|
lookup_key: Joi.string().empty(''),
|
|
149
154
|
});
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
|
|
156
|
+
// search prices — static route, registered before /:id to avoid shadowing
|
|
157
|
+
const searchSchema = createListParamSchema<{
|
|
158
|
+
query: string;
|
|
159
|
+
}>({
|
|
160
|
+
query: Joi.string(),
|
|
161
|
+
});
|
|
162
|
+
app.get('/search', auth, async (c) => {
|
|
163
|
+
const query = c.req.query();
|
|
164
|
+
const {
|
|
165
|
+
page,
|
|
166
|
+
pageSize,
|
|
167
|
+
query: searchQuery,
|
|
168
|
+
livemode,
|
|
169
|
+
} = await searchSchema.validateAsync(query, {
|
|
152
170
|
stripUnknown: false,
|
|
153
171
|
allowUnknown: true,
|
|
154
172
|
});
|
|
155
|
-
const where: WhereOptions<Price> = {};
|
|
156
173
|
|
|
157
|
-
|
|
158
|
-
where.active = active;
|
|
159
|
-
}
|
|
174
|
+
const where = getWhereFromQuery(searchQuery);
|
|
160
175
|
if (typeof livemode === 'boolean') {
|
|
161
176
|
where.livemode = livemode;
|
|
162
177
|
}
|
|
163
|
-
['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
|
|
164
|
-
// @ts-ignore
|
|
165
|
-
if (query[key]) {
|
|
166
|
-
// @ts-ignore
|
|
167
|
-
where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
Object.keys(query)
|
|
172
|
-
.filter((x) => x.startsWith('recurring.'))
|
|
173
|
-
.forEach((key: string) => {
|
|
174
|
-
// @ts-ignore
|
|
175
|
-
where[key] = query[key];
|
|
176
|
-
});
|
|
177
|
-
|
|
178
178
|
const { rows, count } = await Price.findAndCountAll({
|
|
179
179
|
where,
|
|
180
180
|
attributes: ['id'],
|
|
181
|
-
order: getOrder(
|
|
181
|
+
order: getOrder(query, [['created_at', 'DESC']]),
|
|
182
182
|
offset: (page - 1) * pageSize,
|
|
183
183
|
limit: pageSize,
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
return c.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
query
|
|
192
|
-
}>({
|
|
193
|
-
query: Joi.string(),
|
|
194
|
-
});
|
|
195
|
-
router.get('/search', auth, async (req, res) => {
|
|
196
|
-
const { page, pageSize, query, livemode } = await searchSchema.validateAsync(req.query, {
|
|
189
|
+
app.get('/', auth, async (c) => {
|
|
190
|
+
const query = c.req.query();
|
|
191
|
+
const { page, pageSize, active, livemode, ...rest } = await paginationSchema.validateAsync(query, {
|
|
197
192
|
stripUnknown: false,
|
|
198
193
|
allowUnknown: true,
|
|
199
194
|
});
|
|
195
|
+
const where: WhereOptions<Price> = {};
|
|
200
196
|
|
|
201
|
-
|
|
197
|
+
if (typeof active === 'boolean') {
|
|
198
|
+
where.active = active;
|
|
199
|
+
}
|
|
202
200
|
if (typeof livemode === 'boolean') {
|
|
203
201
|
where.livemode = livemode;
|
|
204
202
|
}
|
|
203
|
+
['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
if (rest[key]) {
|
|
206
|
+
// @ts-ignore
|
|
207
|
+
where[key] = rest[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
Object.keys(rest)
|
|
212
|
+
.filter((x) => x.startsWith('recurring.'))
|
|
213
|
+
.forEach((key: string) => {
|
|
214
|
+
// @ts-ignore
|
|
215
|
+
where[key] = rest[key];
|
|
216
|
+
});
|
|
217
|
+
|
|
205
218
|
const { rows, count } = await Price.findAndCountAll({
|
|
206
219
|
where,
|
|
207
220
|
attributes: ['id'],
|
|
208
|
-
order: getOrder(
|
|
221
|
+
order: getOrder(query, [['created_at', 'DESC']]),
|
|
209
222
|
offset: (page - 1) * pageSize,
|
|
210
223
|
limit: pageSize,
|
|
211
224
|
});
|
|
212
225
|
|
|
213
|
-
|
|
226
|
+
return c.json({
|
|
227
|
+
count,
|
|
228
|
+
list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))),
|
|
229
|
+
paging: { page, pageSize },
|
|
230
|
+
});
|
|
214
231
|
});
|
|
215
232
|
|
|
216
|
-
export async function createPrice(payload: any) {
|
|
217
|
-
const raw: Price & { model: 'string' } = payload;
|
|
218
|
-
raw.active = true;
|
|
219
|
-
raw.locked = false;
|
|
220
|
-
raw.livemode = !!payload.livemode;
|
|
221
|
-
raw.currency_id = payload.currency_id;
|
|
222
|
-
|
|
223
|
-
if (!raw.product_id) {
|
|
224
|
-
throw new Error('product_id is required to create a price');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!raw.unit_amount) {
|
|
228
|
-
throw new Error('price unit_amount is required');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const product = await Product.findByPk(raw.product_id);
|
|
232
|
-
if (!product) {
|
|
233
|
-
throw new Error(`product ${raw.product_id} not found for price`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (product.type === 'credit') {
|
|
237
|
-
const creditConfig = raw.metadata.credit_config;
|
|
238
|
-
if (!creditConfig) {
|
|
239
|
-
throw new Error('credit_config is required');
|
|
240
|
-
}
|
|
241
|
-
const { error, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
|
|
242
|
-
if (error) {
|
|
243
|
-
throw new Error(`credit_config is invalid: ${error.message}`);
|
|
244
|
-
}
|
|
245
|
-
raw.metadata.credit_config = creditConfigValue;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
249
|
-
const currency = currencies.find((x) => x.id === raw.currency_id);
|
|
250
|
-
if (!currency) {
|
|
251
|
-
throw new Error(`currency used in price or not active: ${raw.currency_id}`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (Array.isArray(raw.currency_options) === false) {
|
|
255
|
-
raw.currency_options = [];
|
|
256
|
-
}
|
|
257
|
-
if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
|
|
258
|
-
raw.currency_options.unshift({
|
|
259
|
-
currency_id: raw.currency_id,
|
|
260
|
-
unit_amount: raw.unit_amount,
|
|
261
|
-
tiers: null,
|
|
262
|
-
custom_unit_amount: null,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
|
|
267
|
-
raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
|
|
268
|
-
const isRecurring = payload.type === 'recurring';
|
|
269
|
-
const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
|
|
270
|
-
(raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
|
|
271
|
-
isRecurring
|
|
272
|
-
);
|
|
273
|
-
if (!validate) {
|
|
274
|
-
throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
|
|
275
|
-
}
|
|
276
|
-
if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
|
|
277
|
-
raw.recurring.usage_type = 'licensed';
|
|
278
|
-
}
|
|
279
|
-
const price = await Price.insert(raw);
|
|
280
|
-
return getExpandedPrice(price.id as string);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
233
|
const priceQuantitySchema = Joi.object({
|
|
284
234
|
quantity_available: Joi.number().integer().min(0).optional().default(0),
|
|
285
235
|
quantity_limit_per_checkout: Joi.number().integer().min(0).optional().default(0),
|
|
@@ -302,69 +252,58 @@ const priceAmountSchema = Joi.object({
|
|
|
302
252
|
// FIXME: @wangshijun use schema validation
|
|
303
253
|
// create price
|
|
304
254
|
// eslint-disable-next-line consistent-return
|
|
305
|
-
|
|
255
|
+
app.post('/', auth, async (c) => {
|
|
256
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
306
257
|
try {
|
|
307
|
-
const { error } = priceQuantitySchema.validate(
|
|
308
|
-
pick(req.body, ['quantity_available', 'quantity_limit_per_checkout'])
|
|
309
|
-
);
|
|
258
|
+
const { error } = priceQuantitySchema.validate(pick(body, ['quantity_available', 'quantity_limit_per_checkout']));
|
|
310
259
|
if (error) {
|
|
311
|
-
return
|
|
260
|
+
return c.json({ error: `Price create request invalid: ${error.message}` }, 400);
|
|
312
261
|
}
|
|
313
262
|
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
314
|
-
pick(
|
|
263
|
+
pick(body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
|
|
315
264
|
);
|
|
316
265
|
if (priceAmountError) {
|
|
317
|
-
return
|
|
266
|
+
return c.json({ error: `Price create request invalid: ${priceAmountError.message}` }, 400);
|
|
318
267
|
}
|
|
319
|
-
if (
|
|
320
|
-
const { error: metadataError } = MetadataSchema.validate(
|
|
268
|
+
if ((body as any).metadata) {
|
|
269
|
+
const { error: metadataError } = MetadataSchema.validate((body as any).metadata);
|
|
321
270
|
if (metadataError) {
|
|
322
|
-
return
|
|
271
|
+
return c.json({ error: `metadata invalid: ${metadataError.message}` }, 400);
|
|
323
272
|
}
|
|
324
273
|
}
|
|
325
274
|
|
|
326
|
-
if (
|
|
327
|
-
const currencyIds =
|
|
328
|
-
if (
|
|
329
|
-
currencyIds.push(
|
|
275
|
+
if ((body as any).pricing_type === 'dynamic') {
|
|
276
|
+
const currencyIds = (body as any).currency_options?.map((x: any) => x.currency_id).filter(Boolean) || [];
|
|
277
|
+
if ((body as any).currency_id) {
|
|
278
|
+
currencyIds.push((body as any).currency_id);
|
|
330
279
|
}
|
|
331
280
|
const validationError = await validateDynamicPricingCurrencies(currencyIds);
|
|
332
281
|
if (validationError) {
|
|
333
|
-
return
|
|
282
|
+
return c.json({ error: validationError }, 400);
|
|
334
283
|
}
|
|
335
284
|
}
|
|
336
285
|
|
|
337
286
|
const result = await createPrice({
|
|
338
|
-
...
|
|
339
|
-
livemode: !!
|
|
340
|
-
currency_id:
|
|
341
|
-
created_via:
|
|
287
|
+
...(body as any),
|
|
288
|
+
livemode: !!c.get('livemode'),
|
|
289
|
+
currency_id: (body as any).currency_id || c.get('baseCurrency').id,
|
|
290
|
+
created_via: c.get('user')?.via as string,
|
|
342
291
|
quantity_sold: 0,
|
|
343
292
|
});
|
|
344
293
|
|
|
345
|
-
logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy:
|
|
346
|
-
|
|
294
|
+
logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: c.get('user')?.did });
|
|
295
|
+
return c.json(result);
|
|
347
296
|
} catch (err) {
|
|
348
|
-
logger.error('Error creating price', { error: err.message, request:
|
|
349
|
-
|
|
297
|
+
logger.error('Error creating price', { error: (err as any).message, request: body });
|
|
298
|
+
return c.json({ error: (err as any).message }, 400);
|
|
350
299
|
}
|
|
351
300
|
});
|
|
352
301
|
|
|
353
|
-
// get price
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
if (doc) {
|
|
357
|
-
res.json(doc);
|
|
358
|
-
} else {
|
|
359
|
-
res.status(404).json(null);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// get price used status
|
|
364
|
-
router.get('/:id/used', auth, async (req, res) => {
|
|
365
|
-
const price = await Price.findByPkOrLookupKey(req.params.id as string);
|
|
302
|
+
// get price used status — static sub-path, registered before /:id
|
|
303
|
+
app.get('/:id/used', auth, async (c) => {
|
|
304
|
+
const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
|
|
366
305
|
if (!price) {
|
|
367
|
-
return
|
|
306
|
+
return c.json({ error: 'Price not found' }, 404);
|
|
368
307
|
}
|
|
369
308
|
|
|
370
309
|
const used = await price.isUsed(true);
|
|
@@ -372,11 +311,11 @@ router.get('/:id/used', auth, async (req, res) => {
|
|
|
372
311
|
await price.update({ locked: false });
|
|
373
312
|
}
|
|
374
313
|
|
|
375
|
-
return
|
|
314
|
+
return c.json({ used });
|
|
376
315
|
});
|
|
377
316
|
|
|
378
|
-
|
|
379
|
-
const price = await Price.findByPkOrLookupKey(req.
|
|
317
|
+
app.get('/:id/upsell', auth, async (c) => {
|
|
318
|
+
const price = await Price.findByPkOrLookupKey(c.req.param('id') as string, {
|
|
380
319
|
include: [{ model: PaymentCurrency, as: 'currency' }],
|
|
381
320
|
});
|
|
382
321
|
|
|
@@ -386,55 +325,67 @@ router.get('/:id/upsell', auth, async (req, res) => {
|
|
|
386
325
|
include: [{ model: PaymentCurrency, as: 'currency' }],
|
|
387
326
|
});
|
|
388
327
|
const upsells = prices.filter((x) => canUpsell(price, x));
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
328
|
+
return c.json(upsells);
|
|
329
|
+
}
|
|
330
|
+
return c.json(null);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// get price detail
|
|
334
|
+
app.get('/:id', auth, async (c) => {
|
|
335
|
+
const doc = await getExpandedPrice(c.req.param('id') as string);
|
|
336
|
+
if (doc) {
|
|
337
|
+
return c.json(doc);
|
|
392
338
|
}
|
|
339
|
+
return c.json(null, 404);
|
|
393
340
|
});
|
|
394
341
|
|
|
395
342
|
// update price
|
|
396
343
|
// FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
|
|
397
|
-
|
|
344
|
+
app.put('/:id', auth, async (c) => {
|
|
345
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
398
346
|
const quantityKeys = ['quantity_available', 'quantity_limit_per_checkout'];
|
|
399
|
-
const { error } = priceQuantitySchema.validate(pick(
|
|
347
|
+
const { error } = priceQuantitySchema.validate(pick(body, quantityKeys));
|
|
400
348
|
if (error) {
|
|
401
|
-
return
|
|
349
|
+
return c.json({ error: `Price update request invalid: ${error.message}` }, 400);
|
|
402
350
|
}
|
|
403
351
|
|
|
404
352
|
const { error: priceAmountError } = priceAmountSchema.validate(
|
|
405
|
-
pick(
|
|
353
|
+
pick(body, ['currency_options', 'unit_amount', 'nickname', 'lookup_key'])
|
|
406
354
|
);
|
|
407
355
|
if (priceAmountError) {
|
|
408
|
-
return
|
|
356
|
+
return c.json({ error: `Price update request invalid: ${priceAmountError.message}` }, 400);
|
|
409
357
|
}
|
|
410
|
-
const doc = await Price.findByPkOrLookupKey(req.
|
|
358
|
+
const doc = await Price.findByPkOrLookupKey(c.req.param('id') as string);
|
|
411
359
|
|
|
412
360
|
if (!doc) {
|
|
413
|
-
return
|
|
361
|
+
return c.json({ error: 'price not found' }, 404);
|
|
414
362
|
}
|
|
415
363
|
|
|
416
364
|
const product = await Product.findByPk(doc.product_id);
|
|
417
365
|
if (!product) {
|
|
418
|
-
return
|
|
366
|
+
return c.json({ error: 'product not found' }, 404);
|
|
419
367
|
}
|
|
420
368
|
|
|
421
369
|
if (doc.active === false) {
|
|
422
|
-
return
|
|
370
|
+
return c.json({ error: 'price archived' }, 403);
|
|
423
371
|
}
|
|
424
372
|
|
|
425
|
-
if (
|
|
373
|
+
if (
|
|
374
|
+
Number((body as any).quantity_available) > 0 &&
|
|
375
|
+
Number((body as any).quantity_available) < Number(doc.quantity_sold)
|
|
376
|
+
) {
|
|
426
377
|
// 可售数量不得小于已售数量
|
|
427
|
-
return
|
|
378
|
+
return c.json({ error: 'the available quantity cannot be less than the quantity sold' }, 400);
|
|
428
379
|
}
|
|
429
380
|
|
|
430
|
-
const locked = doc.locked &&
|
|
431
|
-
const { error: metadataError } = MetadataSchema.validate(
|
|
381
|
+
const locked = doc.locked && !allowChangeLockedPrice();
|
|
382
|
+
const { error: metadataError } = MetadataSchema.validate((body as any).metadata);
|
|
432
383
|
if (metadataError) {
|
|
433
|
-
return
|
|
384
|
+
return c.json({ error: `metadata invalid: ${metadataError.message}` }, 400);
|
|
434
385
|
}
|
|
435
386
|
const updates: Partial<Price> = Price.formatBeforeSave(
|
|
436
387
|
pick(
|
|
437
|
-
|
|
388
|
+
body,
|
|
438
389
|
locked
|
|
439
390
|
? ['nickname', 'description', 'metadata', 'upsell', 'lookup_key', ...quantityKeys]
|
|
440
391
|
: [
|
|
@@ -465,7 +416,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
465
416
|
if (updates.lookup_key) {
|
|
466
417
|
const exist = await Price.findOne({ where: { lookup_key: updates.lookup_key } });
|
|
467
418
|
if (exist && exist.id !== doc.id) {
|
|
468
|
-
return
|
|
419
|
+
return c.json({ error: `lookup_key ${updates.lookup_key} already used by ${exist.id}` }, 400);
|
|
469
420
|
}
|
|
470
421
|
}
|
|
471
422
|
|
|
@@ -473,11 +424,11 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
473
424
|
// Merge with existing credit_config if not provided
|
|
474
425
|
const creditConfig = updates.metadata.credit_config || doc.metadata?.credit_config;
|
|
475
426
|
if (!creditConfig) {
|
|
476
|
-
return
|
|
427
|
+
return c.json({ error: 'credit_config is required' }, 400);
|
|
477
428
|
}
|
|
478
429
|
const { error: creditConfigError, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
|
|
479
430
|
if (creditConfigError) {
|
|
480
|
-
return
|
|
431
|
+
return c.json({ error: `credit_config is invalid: ${creditConfigError.message}` }, 400);
|
|
481
432
|
}
|
|
482
433
|
updates.metadata.credit_config = creditConfigValue;
|
|
483
434
|
}
|
|
@@ -486,9 +437,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
486
437
|
const currency =
|
|
487
438
|
currencies.find((x) => x.id === updates?.currency_id || '') || currencies.find((x) => x.id === doc.currency_id);
|
|
488
439
|
if (!currency) {
|
|
489
|
-
return
|
|
490
|
-
.
|
|
491
|
-
|
|
440
|
+
return c.json(
|
|
441
|
+
{ error: `currency used in price not found or not active: ${updates?.currency_id || doc.currency_id}` },
|
|
442
|
+
400
|
|
443
|
+
);
|
|
492
444
|
}
|
|
493
445
|
if (updates.unit_amount) {
|
|
494
446
|
updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
|
|
@@ -519,9 +471,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
519
471
|
isRecurring
|
|
520
472
|
);
|
|
521
473
|
if (!validate) {
|
|
522
|
-
return
|
|
523
|
-
.
|
|
524
|
-
|
|
474
|
+
return c.json(
|
|
475
|
+
{ error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` },
|
|
476
|
+
400
|
|
477
|
+
);
|
|
525
478
|
}
|
|
526
479
|
|
|
527
480
|
const pricingType = updates.pricing_type || doc.pricing_type;
|
|
@@ -532,112 +485,182 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
532
485
|
currencyIds.length ? currencyIds : [updates.currency_id || doc.currency_id]
|
|
533
486
|
);
|
|
534
487
|
if (validationError) {
|
|
535
|
-
return
|
|
488
|
+
return c.json({ error: validationError }, 400);
|
|
536
489
|
}
|
|
537
490
|
}
|
|
538
491
|
|
|
539
492
|
try {
|
|
540
493
|
await doc.update(Price.formatBeforeSave(updates));
|
|
541
|
-
logger.info(`Price updated: ${req.
|
|
542
|
-
|
|
494
|
+
logger.info(`Price updated: ${c.req.param('id')}`, {
|
|
495
|
+
priceId: c.req.param('id'),
|
|
496
|
+
updates,
|
|
497
|
+
requestedBy: c.get('user')?.did,
|
|
498
|
+
});
|
|
499
|
+
return c.json(await getExpandedPrice(c.req.param('id') as string));
|
|
543
500
|
} catch (err) {
|
|
544
|
-
logger.error('Error updating price', { error: err.message, request:
|
|
545
|
-
return
|
|
501
|
+
logger.error('Error updating price', { error: (err as any).message, request: body });
|
|
502
|
+
return c.json({ error: (err as any).message }, 400);
|
|
546
503
|
}
|
|
547
504
|
});
|
|
548
505
|
|
|
549
506
|
// archive
|
|
550
|
-
|
|
551
|
-
const price = await Price.findByPkOrLookupKey(req.
|
|
507
|
+
app.put('/:id/archive', auth, async (c) => {
|
|
508
|
+
const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
|
|
552
509
|
|
|
553
510
|
if (!price) {
|
|
554
|
-
return
|
|
511
|
+
return c.json({ error: 'price not found' }, 404);
|
|
555
512
|
}
|
|
556
513
|
|
|
557
514
|
if (price.active === false) {
|
|
558
|
-
return
|
|
515
|
+
return c.json({ error: 'price already archived' }, 403);
|
|
559
516
|
}
|
|
560
517
|
|
|
561
518
|
if (price.locked) {
|
|
562
|
-
return
|
|
519
|
+
return c.json({ error: 'price locked' }, 403);
|
|
563
520
|
}
|
|
564
521
|
|
|
565
522
|
await price.update({ active: false });
|
|
566
523
|
|
|
567
|
-
logger.info(`Price archived: ${req.
|
|
568
|
-
return
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
// delete price
|
|
572
|
-
router.delete('/:id', auth, async (req, res) => {
|
|
573
|
-
try {
|
|
574
|
-
const price = await Price.findByPkOrLookupKey(req.params.id as string);
|
|
575
|
-
|
|
576
|
-
if (!price) {
|
|
577
|
-
return res.status(404).json({ error: 'Can not delete none existing price' });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (price.locked) {
|
|
581
|
-
return res.status(403).json({ error: 'Can not delete locked price' });
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (await price.isUsed(true)) {
|
|
585
|
-
return res.status(403).json({ error: 'Can not delete price used by other resources' });
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
await price.destroy();
|
|
589
|
-
return res.json(price);
|
|
590
|
-
} catch (err) {
|
|
591
|
-
logger.error('delete price error', err);
|
|
592
|
-
return res.status(400).json({ error: err.message });
|
|
593
|
-
}
|
|
524
|
+
logger.info(`Price archived: ${c.req.param('id')}`, { priceId: c.req.param('id'), requestedBy: c.get('user')?.did });
|
|
525
|
+
return c.json(await getExpandedPrice(c.req.param('id') as string));
|
|
594
526
|
});
|
|
595
527
|
|
|
596
528
|
const priceInventorySchema = Joi.object({
|
|
597
529
|
quantity: Joi.number().integer().min(0).required(),
|
|
598
530
|
action: Joi.string().valid('decrement', 'increment').required(),
|
|
599
531
|
});
|
|
600
|
-
|
|
532
|
+
app.put('/:id/inventory', auth, async (c) => {
|
|
533
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
601
534
|
try {
|
|
602
|
-
const { error } = priceInventorySchema.validate(
|
|
535
|
+
const { error } = priceInventorySchema.validate(body);
|
|
603
536
|
if (error) {
|
|
604
|
-
return
|
|
537
|
+
return c.json({ error: `Price inventory update request invalid: ${error.message}` }, 400);
|
|
605
538
|
}
|
|
606
|
-
const price = await Price.findByPkOrLookupKey(req.
|
|
539
|
+
const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
|
|
607
540
|
if (!price) {
|
|
608
|
-
return
|
|
541
|
+
return c.json({ error: 'price not found' }, 404);
|
|
609
542
|
}
|
|
610
|
-
const quantity = Number(
|
|
543
|
+
const quantity = Number((body as any).quantity);
|
|
611
544
|
const limitPerCheckQuantity = price.quantity_limit_per_checkout;
|
|
612
545
|
if (limitPerCheckQuantity > 0 && quantity > limitPerCheckQuantity) {
|
|
613
|
-
return
|
|
614
|
-
.status(400)
|
|
615
|
-
.json({ error: `quantity ${quantity} exceeds limit per checkout ${limitPerCheckQuantity}` });
|
|
546
|
+
return c.json({ error: `quantity ${quantity} exceeds limit per checkout ${limitPerCheckQuantity}` }, 400);
|
|
616
547
|
}
|
|
617
548
|
|
|
618
|
-
if (
|
|
549
|
+
if ((body as any).action === 'decrement') {
|
|
619
550
|
if (quantity > price.quantity_sold) {
|
|
620
|
-
return
|
|
551
|
+
return c.json({ error: 'decrementing quantity exceeds sold quantity' }, 400);
|
|
621
552
|
}
|
|
622
|
-
await price.decrement('quantity_sold', { by:
|
|
553
|
+
await price.decrement('quantity_sold', { by: (body as any).quantity });
|
|
623
554
|
}
|
|
624
|
-
if (
|
|
555
|
+
if ((body as any).action === 'increment') {
|
|
625
556
|
if (price.quantity_available > 0 && price.quantity_sold + quantity > price.quantity_available) {
|
|
626
|
-
return
|
|
557
|
+
return c.json({ error: 'incrementing sold quantity exceeds available quantity' }, 400);
|
|
627
558
|
}
|
|
628
|
-
await price.increment('quantity_sold', { by:
|
|
559
|
+
await price.increment('quantity_sold', { by: (body as any).quantity });
|
|
629
560
|
}
|
|
630
|
-
logger.info(`Price inventory updated: ${req.
|
|
631
|
-
priceId: req.
|
|
632
|
-
action:
|
|
633
|
-
quantity:
|
|
634
|
-
requestedBy:
|
|
561
|
+
logger.info(`Price inventory updated: ${c.req.param('id')}`, {
|
|
562
|
+
priceId: c.req.param('id'),
|
|
563
|
+
action: (body as any).action,
|
|
564
|
+
quantity: (body as any).quantity,
|
|
565
|
+
requestedBy: c.get('user')?.did,
|
|
635
566
|
});
|
|
636
|
-
return
|
|
567
|
+
return c.json(await getExpandedPrice(c.req.param('id') as string));
|
|
637
568
|
} catch (err) {
|
|
638
569
|
logger.error('update price inventory error', err);
|
|
639
|
-
return
|
|
570
|
+
return c.json({ error: (err as any).message }, 400);
|
|
640
571
|
}
|
|
641
572
|
});
|
|
642
573
|
|
|
643
|
-
|
|
574
|
+
// delete price
|
|
575
|
+
app.delete('/:id', auth, async (c) => {
|
|
576
|
+
try {
|
|
577
|
+
const price = await Price.findByPkOrLookupKey(c.req.param('id') as string);
|
|
578
|
+
|
|
579
|
+
if (!price) {
|
|
580
|
+
return c.json({ error: 'Can not delete none existing price' }, 404);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (price.locked) {
|
|
584
|
+
return c.json({ error: 'Can not delete locked price' }, 403);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (await price.isUsed(true)) {
|
|
588
|
+
return c.json({ error: 'Can not delete price used by other resources' }, 403);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
await price.destroy();
|
|
592
|
+
return c.json(price);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
logger.error('delete price error', err);
|
|
595
|
+
return c.json({ error: (err as any).message }, 400);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
export async function createPrice(payload: any) {
|
|
600
|
+
const raw: Price & { model: 'string' } = payload;
|
|
601
|
+
raw.active = true;
|
|
602
|
+
raw.locked = false;
|
|
603
|
+
raw.livemode = !!payload.livemode;
|
|
604
|
+
raw.currency_id = payload.currency_id;
|
|
605
|
+
|
|
606
|
+
if (!raw.product_id) {
|
|
607
|
+
throw new Error('product_id is required to create a price');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!raw.unit_amount) {
|
|
611
|
+
throw new Error('price unit_amount is required');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const product = await Product.findByPk(raw.product_id);
|
|
615
|
+
if (!product) {
|
|
616
|
+
throw new Error(`product ${raw.product_id} not found for price`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (product.type === 'credit') {
|
|
620
|
+
const creditConfig = raw.metadata.credit_config;
|
|
621
|
+
if (!creditConfig) {
|
|
622
|
+
throw new Error('credit_config is required');
|
|
623
|
+
}
|
|
624
|
+
const { error, value: creditConfigValue } = CreditConfigSchema.validate(creditConfig);
|
|
625
|
+
if (error) {
|
|
626
|
+
throw new Error(`credit_config is invalid: ${error.message}`);
|
|
627
|
+
}
|
|
628
|
+
raw.metadata.credit_config = creditConfigValue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const currencies = await PaymentCurrency.findAll({ where: { active: true } });
|
|
632
|
+
const currency = currencies.find((x) => x.id === raw.currency_id);
|
|
633
|
+
if (!currency) {
|
|
634
|
+
throw new Error(`currency used in price or not active: ${raw.currency_id}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (Array.isArray(raw.currency_options) === false) {
|
|
638
|
+
raw.currency_options = [];
|
|
639
|
+
}
|
|
640
|
+
if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
|
|
641
|
+
raw.currency_options.unshift({
|
|
642
|
+
currency_id: raw.currency_id,
|
|
643
|
+
unit_amount: raw.unit_amount,
|
|
644
|
+
tiers: null,
|
|
645
|
+
custom_unit_amount: null,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
|
|
650
|
+
raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
|
|
651
|
+
const isRecurring = payload.type === 'recurring';
|
|
652
|
+
const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
|
|
653
|
+
(raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
|
|
654
|
+
isRecurring
|
|
655
|
+
);
|
|
656
|
+
if (!validate) {
|
|
657
|
+
throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
|
|
658
|
+
}
|
|
659
|
+
if (isRecurring && raw.recurring && !raw.recurring.usage_type) {
|
|
660
|
+
raw.recurring.usage_type = 'licensed';
|
|
661
|
+
}
|
|
662
|
+
const price = await Price.insert(raw);
|
|
663
|
+
return getExpandedPrice(price.id as string);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export default app;
|