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,29 +1,33 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
|
+
// Phase 3 (express→hono) — hono fork of routes/invoices.ts. Sub-app with
|
|
3
|
+
// routes relative to /api/invoices (mounted via mountResourceGroup). The
|
|
4
|
+
// business logic is unchanged; only the express plumbing becomes hono:
|
|
5
|
+
// req.body → c.get('sanitizedBody') ?? {}; res.status(n).json(x) → c.json(x, n).
|
|
2
6
|
import { isValid } from '@arcblock/did';
|
|
3
|
-
import {
|
|
7
|
+
import { Hono } from 'hono';
|
|
4
8
|
import Joi from 'joi';
|
|
5
9
|
import pick from 'lodash/pick';
|
|
6
10
|
import { Op } from 'sequelize';
|
|
7
11
|
|
|
8
12
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
9
|
-
import { syncStripeInvoice } from '
|
|
10
|
-
import { syncStripePayment } from '
|
|
11
|
-
import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '
|
|
12
|
-
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '
|
|
13
|
-
import { authenticate } from '
|
|
14
|
-
import { expandLineItems } from '
|
|
15
|
-
import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '
|
|
16
|
-
import dayjs from '
|
|
17
|
-
import { Customer } from '
|
|
18
|
-
import { Invoice } from '
|
|
19
|
-
import { InvoiceItem } from '
|
|
20
|
-
import { PaymentCurrency } from '
|
|
21
|
-
import { PaymentIntent } from '
|
|
22
|
-
import { PaymentMethod } from '
|
|
23
|
-
import { Price } from '
|
|
24
|
-
import { Product } from '
|
|
25
|
-
import { Subscription } from '
|
|
26
|
-
import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '
|
|
13
|
+
import { syncStripeInvoice } from '../../integrations/stripe/handlers/invoice';
|
|
14
|
+
import { syncStripePayment } from '../../integrations/stripe/handlers/payment-intent';
|
|
15
|
+
import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../../integrations/stripe/resource';
|
|
16
|
+
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../../libs/api';
|
|
17
|
+
import { authenticate } from '../../middlewares/hono/security';
|
|
18
|
+
import { expandLineItems } from '../../libs/session';
|
|
19
|
+
import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../../libs/util';
|
|
20
|
+
import dayjs from '../../libs/dayjs';
|
|
21
|
+
import { Customer } from '../../store/models/customer';
|
|
22
|
+
import { Invoice } from '../../store/models/invoice';
|
|
23
|
+
import { InvoiceItem } from '../../store/models/invoice-item';
|
|
24
|
+
import { PaymentCurrency } from '../../store/models/payment-currency';
|
|
25
|
+
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
26
|
+
import { PaymentMethod } from '../../store/models/payment-method';
|
|
27
|
+
import { Price } from '../../store/models/price';
|
|
28
|
+
import { Product } from '../../store/models/product';
|
|
29
|
+
import { Subscription } from '../../store/models/subscription';
|
|
30
|
+
import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../../libs/invoice';
|
|
27
31
|
import {
|
|
28
32
|
CheckoutSession,
|
|
29
33
|
PaymentLink,
|
|
@@ -34,12 +38,12 @@ import {
|
|
|
34
38
|
CreditGrant,
|
|
35
39
|
TaxRate,
|
|
36
40
|
PriceQuote,
|
|
37
|
-
} from '
|
|
38
|
-
import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '
|
|
39
|
-
import logger from '
|
|
40
|
-
import { returnOverdraftProtectionQueue, returnStakeQueue } from '
|
|
41
|
-
import { checkRemainingStake } from '
|
|
42
|
-
import { getExchangeRateService } from '
|
|
41
|
+
} from '../../store/models';
|
|
42
|
+
import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../../libs/pagination';
|
|
43
|
+
import logger from '../../libs/logger';
|
|
44
|
+
import { returnOverdraftProtectionQueue, returnStakeQueue } from '../../queues/subscription';
|
|
45
|
+
import { checkRemainingStake } from '../../libs/subscription';
|
|
46
|
+
import { getExchangeRateService } from '../../libs/exchange-rate';
|
|
43
47
|
|
|
44
48
|
// Simple format amount helper for backend
|
|
45
49
|
function formatAmountForDisplay(amount: string, decimal: number, symbol: string): string {
|
|
@@ -47,7 +51,7 @@ function formatAmountForDisplay(amount: string, decimal: number, symbol: string)
|
|
|
47
51
|
return `${parseFloat(tokenValue).toFixed(Math.min(decimal, 6))} ${symbol}`;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
const
|
|
54
|
+
const app = new Hono();
|
|
51
55
|
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
52
56
|
const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true, embed: true });
|
|
53
57
|
const authPortal = authenticate<Invoice>({
|
|
@@ -235,7 +239,7 @@ const attachQuoteMetadataToLines = (lines: any[] | undefined, quotesById: Map<st
|
|
|
235
239
|
});
|
|
236
240
|
};
|
|
237
241
|
|
|
238
|
-
|
|
242
|
+
app.get('/', authMine, async (c) => {
|
|
239
243
|
try {
|
|
240
244
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
241
245
|
const {
|
|
@@ -249,7 +253,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
249
253
|
include_overdraft_protection = true,
|
|
250
254
|
include_quote = false,
|
|
251
255
|
...query
|
|
252
|
-
} = await schema.validateAsync(req.query, {
|
|
256
|
+
} = await schema.validateAsync(c.req.query(), {
|
|
253
257
|
stripUnknown: false,
|
|
254
258
|
allowUnknown: true,
|
|
255
259
|
});
|
|
@@ -270,8 +274,8 @@ router.get('/', authMine, async (req, res) => {
|
|
|
270
274
|
if (ignore_zero) {
|
|
271
275
|
where.subtotal = { [Op.ne]: '0' };
|
|
272
276
|
}
|
|
273
|
-
if (query.customer_id) {
|
|
274
|
-
where.customer_id = query.customer_id;
|
|
277
|
+
if (c.get('customer_id') ?? query.customer_id) {
|
|
278
|
+
where.customer_id = c.get('customer_id') ?? query.customer_id;
|
|
275
279
|
}
|
|
276
280
|
if (query.currency_id) {
|
|
277
281
|
where.currency_id = query.currency_id;
|
|
@@ -281,8 +285,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
281
285
|
if (customer) {
|
|
282
286
|
where.customer_id = customer.id;
|
|
283
287
|
} else {
|
|
284
|
-
|
|
285
|
-
return;
|
|
288
|
+
return c.json({ count: 0, list: [], paging: { page, pageSize } });
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
if (query.subscription_id) {
|
|
@@ -333,7 +336,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
333
336
|
}),
|
|
334
337
|
Invoice.findAll({
|
|
335
338
|
where,
|
|
336
|
-
order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
|
|
339
|
+
order: getOrder(c.req.query(), [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
|
|
337
340
|
limit: pageSize,
|
|
338
341
|
offset: (page - 1) * pageSize,
|
|
339
342
|
subQuery: false,
|
|
@@ -375,12 +378,11 @@ router.get('/', authMine, async (req, res) => {
|
|
|
375
378
|
}),
|
|
376
379
|
]);
|
|
377
380
|
|
|
378
|
-
|
|
381
|
+
return c.json({
|
|
379
382
|
count,
|
|
380
383
|
list,
|
|
381
384
|
paging: { page, pageSize },
|
|
382
385
|
});
|
|
383
|
-
return;
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
const sources: DataSource<Invoice>[] = [];
|
|
@@ -393,7 +395,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
393
395
|
fetch: async (limit: number, offset: number = 0) => {
|
|
394
396
|
const result = await Invoice.findAll({
|
|
395
397
|
where,
|
|
396
|
-
order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
|
|
398
|
+
order: getOrder(c.req.query(), [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
|
|
397
399
|
limit,
|
|
398
400
|
offset,
|
|
399
401
|
include: [
|
|
@@ -541,7 +543,7 @@ router.get('/', authMine, async (req, res) => {
|
|
|
541
543
|
defaultTimeOrderBy(query.o === 'asc' ? 'asc' : 'desc')
|
|
542
544
|
);
|
|
543
545
|
|
|
544
|
-
|
|
546
|
+
return c.json({
|
|
545
547
|
count: result.total,
|
|
546
548
|
list: result.data,
|
|
547
549
|
paging: result.paging,
|
|
@@ -549,15 +551,15 @@ router.get('/', authMine, async (req, res) => {
|
|
|
549
551
|
} catch (err) {
|
|
550
552
|
logger.error('Failed to fetch invoices', {
|
|
551
553
|
error: err,
|
|
552
|
-
page: req.query
|
|
553
|
-
pageSize: req.query
|
|
554
|
-
include_staking: req.query
|
|
555
|
-
subscription_id: req.query
|
|
554
|
+
page: c.req.query('page'),
|
|
555
|
+
pageSize: c.req.query('pageSize'),
|
|
556
|
+
include_staking: c.req.query('include_staking'),
|
|
557
|
+
subscription_id: c.req.query('subscription_id'),
|
|
556
558
|
});
|
|
557
|
-
|
|
559
|
+
return c.json({
|
|
558
560
|
count: 0,
|
|
559
561
|
list: [],
|
|
560
|
-
paging: { page: Number(req.query
|
|
562
|
+
paging: { page: Number(c.req.query('page') || 1), pageSize: Number(c.req.query('pageSize') || 10) },
|
|
561
563
|
});
|
|
562
564
|
}
|
|
563
565
|
});
|
|
@@ -574,16 +576,17 @@ const rechargeSchema = createListParamSchema<{
|
|
|
574
576
|
recharge_address: Joi.string().empty(''),
|
|
575
577
|
});
|
|
576
578
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
+
// Static path — registered before /:id so hono matches it first.
|
|
580
|
+
app.get('/recharge', authMine, async (c) => {
|
|
581
|
+
const { page, pageSize, ...query } = await rechargeSchema.validateAsync(c.req.query(), {
|
|
579
582
|
stripUnknown: false,
|
|
580
583
|
allowUnknown: true,
|
|
581
584
|
});
|
|
582
585
|
const where = getWhereFromKvQuery(query.q);
|
|
583
|
-
if (query.customer_id) {
|
|
584
|
-
const customer = await Customer.findByPkOrDid(query.customer_id);
|
|
586
|
+
if (c.get('customer_id') ?? query.customer_id) {
|
|
587
|
+
const customer = await Customer.findByPkOrDid(c.get('customer_id') ?? query.customer_id);
|
|
585
588
|
if (!customer) {
|
|
586
|
-
return
|
|
589
|
+
return c.json({ error: 'Customer not found' }, 404);
|
|
587
590
|
}
|
|
588
591
|
where.customer_id = customer.id;
|
|
589
592
|
}
|
|
@@ -603,23 +606,24 @@ router.get('/recharge', authMine, async (req, res) => {
|
|
|
603
606
|
},
|
|
604
607
|
offset: (page - 1) * pageSize,
|
|
605
608
|
limit: pageSize,
|
|
606
|
-
order: getOrder(req.query, [['created_at', 'DESC']]),
|
|
609
|
+
order: getOrder(c.req.query(), [['created_at', 'DESC']]),
|
|
607
610
|
include: [
|
|
608
611
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
609
612
|
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
610
613
|
],
|
|
611
614
|
});
|
|
612
615
|
|
|
613
|
-
return
|
|
616
|
+
return c.json({ count, list: invoices, paging: { page, pageSize } });
|
|
614
617
|
} catch (err) {
|
|
615
618
|
logger.error(err);
|
|
616
|
-
return
|
|
619
|
+
return c.json({ error: err.message }, 400);
|
|
617
620
|
}
|
|
618
621
|
});
|
|
619
622
|
|
|
620
623
|
const searchSchema = createListParamSchema<{}>({});
|
|
621
|
-
|
|
622
|
-
|
|
624
|
+
// Static path — registered before /:id so hono matches it first.
|
|
625
|
+
app.get('/search', authMine, async (c) => {
|
|
626
|
+
const { page, pageSize, livemode, q, o } = await searchSchema.validateAsync(c.req.query(), {
|
|
623
627
|
stripUnknown: false,
|
|
624
628
|
allowUnknown: true,
|
|
625
629
|
});
|
|
@@ -631,7 +635,7 @@ router.get('/search', authMine, async (req, res) => {
|
|
|
631
635
|
|
|
632
636
|
const { rows: list, count } = await Invoice.findAndCountAll({
|
|
633
637
|
where,
|
|
634
|
-
order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
|
|
638
|
+
order: getOrder(c.req.query(), [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
|
|
635
639
|
offset: (page - 1) * pageSize,
|
|
636
640
|
limit: pageSize,
|
|
637
641
|
distinct: true,
|
|
@@ -641,7 +645,7 @@ router.get('/search', authMine, async (req, res) => {
|
|
|
641
645
|
],
|
|
642
646
|
});
|
|
643
647
|
|
|
644
|
-
|
|
648
|
+
return c.json({ count, list, paging: { page, pageSize } });
|
|
645
649
|
});
|
|
646
650
|
|
|
647
651
|
const retryUncollectibleSchema = Joi.object({
|
|
@@ -664,14 +668,15 @@ const retryUncollectibleSchema = Joi.object({
|
|
|
664
668
|
.optional(),
|
|
665
669
|
currencyId: Joi.string().trim().allow('').optional(),
|
|
666
670
|
});
|
|
667
|
-
|
|
671
|
+
// Static path — registered before /:id so hono matches it first.
|
|
672
|
+
app.get('/retry-uncollectible', authAdmin, async (c) => {
|
|
668
673
|
try {
|
|
669
|
-
const { error, value } = retryUncollectibleSchema.validate(req.query, {
|
|
674
|
+
const { error, value } = retryUncollectibleSchema.validate(c.req.query(), {
|
|
670
675
|
stripUnknown: true,
|
|
671
676
|
});
|
|
672
677
|
|
|
673
678
|
if (error) {
|
|
674
|
-
return
|
|
679
|
+
return c.json({ error: error.message }, 400);
|
|
675
680
|
}
|
|
676
681
|
|
|
677
682
|
const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = value;
|
|
@@ -684,60 +689,64 @@ router.get('/retry-uncollectible', authAdmin, async (req, res) => {
|
|
|
684
689
|
currencyId,
|
|
685
690
|
});
|
|
686
691
|
|
|
687
|
-
return
|
|
692
|
+
return c.json(result);
|
|
688
693
|
} catch (error) {
|
|
689
694
|
logger.error('Failed to retry uncollectible invoices', { error });
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
695
|
+
return c.json(
|
|
696
|
+
{
|
|
697
|
+
error: 'Failed to retry uncollectible invoices',
|
|
698
|
+
message: error.message,
|
|
699
|
+
},
|
|
700
|
+
500
|
|
701
|
+
);
|
|
694
702
|
}
|
|
695
703
|
});
|
|
696
704
|
|
|
697
|
-
|
|
698
|
-
|
|
705
|
+
// Static path /:id/return-stake — registered before plain /:id so hono matches it first.
|
|
706
|
+
app.get('/:id/return-stake', authAdmin, async (c) => {
|
|
707
|
+
const doc = await Invoice.findByPk(c.req.param('id') as string);
|
|
699
708
|
if (!doc) {
|
|
700
|
-
return
|
|
709
|
+
return c.json({ error: 'Invoice not found' }, 404);
|
|
701
710
|
}
|
|
702
711
|
if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
|
|
703
|
-
return
|
|
712
|
+
return c.json({ error: 'Invoice is not a stake invoice' }, 400);
|
|
704
713
|
}
|
|
705
714
|
const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
|
|
706
715
|
if (!paymentCurrency) {
|
|
707
|
-
return
|
|
716
|
+
return c.json({ error: 'Payment currency not found' }, 400);
|
|
708
717
|
}
|
|
709
718
|
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
710
719
|
if (!paymentMethod) {
|
|
711
|
-
return
|
|
720
|
+
return c.json({ error: 'Payment method not found' }, 400);
|
|
712
721
|
}
|
|
713
722
|
const stakingAddress = doc.metadata?.payment_details?.arcblock?.address;
|
|
714
723
|
if (!stakingAddress) {
|
|
715
|
-
return
|
|
724
|
+
return c.json({ error: 'Staking address not found' }, 400);
|
|
716
725
|
}
|
|
717
726
|
const { staked } = await checkRemainingStake(paymentMethod, paymentCurrency, stakingAddress, '0');
|
|
718
|
-
return
|
|
727
|
+
return c.json(staked);
|
|
719
728
|
});
|
|
720
|
-
|
|
721
|
-
const doc = await Invoice.findByPk(req.
|
|
729
|
+
app.post('/:id/return-stake', authAdmin, async (c) => {
|
|
730
|
+
const doc = await Invoice.findByPk(c.req.param('id') as string);
|
|
722
731
|
if (!doc) {
|
|
723
|
-
return
|
|
732
|
+
return c.json({ error: 'Invoice not found' }, 404);
|
|
724
733
|
}
|
|
725
734
|
if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
|
|
726
|
-
return
|
|
735
|
+
return c.json({ error: 'Invoice is not a stake invoice' }, 400);
|
|
727
736
|
}
|
|
728
737
|
if (doc.status !== 'paid') {
|
|
729
|
-
return
|
|
738
|
+
return c.json({ error: 'Invoice is not paid' }, 400);
|
|
730
739
|
}
|
|
731
740
|
const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
|
|
732
741
|
if (!paymentMethod) {
|
|
733
|
-
return
|
|
742
|
+
return c.json({ error: 'Payment method not found' }, 400);
|
|
734
743
|
}
|
|
735
744
|
if (paymentMethod.type !== 'arcblock') {
|
|
736
|
-
return
|
|
745
|
+
return c.json({ error: 'Can only return stake for arcblock payment method' }, 400);
|
|
737
746
|
}
|
|
738
747
|
const subscription = await Subscription.findByPk(doc.subscription_id);
|
|
739
748
|
if (!subscription) {
|
|
740
|
-
return
|
|
749
|
+
return c.json({ error: 'Subscription not found' }, 400);
|
|
741
750
|
}
|
|
742
751
|
try {
|
|
743
752
|
if (doc.billing_reason === 'stake') {
|
|
@@ -749,26 +758,146 @@ router.post('/:id/return-stake', authAdmin, async (req, res) => {
|
|
|
749
758
|
paymentCurrencyId: doc.currency_id,
|
|
750
759
|
},
|
|
751
760
|
});
|
|
752
|
-
return
|
|
761
|
+
return c.json({ success: true, subscriptionId: subscription.id });
|
|
753
762
|
}
|
|
754
763
|
if (doc.billing_reason === 'stake_overdraft_protection' && subscription.status === 'canceled') {
|
|
755
764
|
await returnOverdraftProtectionQueue.pushAndWait({
|
|
756
765
|
id: `return-overdraft-protection-${subscription.id}`,
|
|
757
766
|
job: { subscriptionId: subscription.id },
|
|
758
767
|
});
|
|
759
|
-
return
|
|
768
|
+
return c.json({ success: true, subscriptionId: subscription.id });
|
|
760
769
|
}
|
|
761
|
-
return
|
|
770
|
+
return c.json({ success: false, error: 'Subscription is not canceled' });
|
|
762
771
|
} catch (error) {
|
|
763
772
|
logger.error('Failed to return stake', { error, subscriptionId: subscription.id, invoiceId: doc.id });
|
|
764
|
-
return
|
|
773
|
+
return c.json({ error: 'Failed to return stake' }, 400);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// Static path /:id/payment-options — registered before plain /:id so hono matches it first.
|
|
778
|
+
app.get('/:id/payment-options', authPortal, async (c) => {
|
|
779
|
+
try {
|
|
780
|
+
const invoice = await Invoice.findByPk(c.req.param('id'), {
|
|
781
|
+
include: [
|
|
782
|
+
{ model: InvoiceItem, as: 'lines' },
|
|
783
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
784
|
+
],
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (!invoice) {
|
|
788
|
+
return c.json({ error: 'Invoice not found' }, 404);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Get all available currencies for this invoice's products
|
|
792
|
+
const invoiceItems = (invoice as any).lines || [];
|
|
793
|
+
const priceIds = invoiceItems.map((item: any) => item.price_id).filter(Boolean);
|
|
794
|
+
|
|
795
|
+
// Get all unique product IDs from prices
|
|
796
|
+
const prices = await Price.findAll({
|
|
797
|
+
where: { id: priceIds },
|
|
798
|
+
include: [{ model: PaymentCurrency, as: 'currencies' }],
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Collect all unique currencies
|
|
802
|
+
const currencyMap = new Map<string, any>();
|
|
803
|
+
const currentCurrencyId = invoice.currency_id;
|
|
804
|
+
|
|
805
|
+
// Add current currency first
|
|
806
|
+
const { paymentCurrency } = invoice as any;
|
|
807
|
+
if (paymentCurrency) {
|
|
808
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
809
|
+
currencyMap.set(paymentCurrency.id, {
|
|
810
|
+
...paymentCurrency.toJSON(),
|
|
811
|
+
method: paymentMethod?.toJSON(),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Add other available currencies from prices
|
|
816
|
+
for (const price of prices) {
|
|
817
|
+
const priceCurrencies = (price as any).currencies || [];
|
|
818
|
+
for (const currency of priceCurrencies) {
|
|
819
|
+
if (!currencyMap.has(currency.id)) {
|
|
820
|
+
// eslint-disable-next-line no-await-in-loop
|
|
821
|
+
const paymentMethod = await PaymentMethod.findByPk(currency.payment_method_id);
|
|
822
|
+
currencyMap.set(currency.id, {
|
|
823
|
+
...currency.toJSON(),
|
|
824
|
+
method: paymentMethod?.toJSON(),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Calculate estimated amounts for each currency
|
|
831
|
+
const exchangeRateService = getExchangeRateService();
|
|
832
|
+
const options = [];
|
|
833
|
+
|
|
834
|
+
for (const [currencyId, currency] of currencyMap) {
|
|
835
|
+
let estimatedAmount = '';
|
|
836
|
+
const isCurrentMethod = currencyId === currentCurrencyId;
|
|
837
|
+
|
|
838
|
+
// For current method, use the invoice amount
|
|
839
|
+
if (isCurrentMethod) {
|
|
840
|
+
estimatedAmount = formatAmountForDisplay(invoice.amount_due, currency.decimal, currency.symbol);
|
|
841
|
+
} else {
|
|
842
|
+
// For other methods, calculate based on USD base amount and current exchange rate
|
|
843
|
+
try {
|
|
844
|
+
// Get USD total from line items
|
|
845
|
+
let usdTotal = 0;
|
|
846
|
+
for (const item of invoiceItems) {
|
|
847
|
+
const itemPrice = prices.find((p) => p.id === item.price_id);
|
|
848
|
+
if (itemPrice?.base_amount) {
|
|
849
|
+
usdTotal += parseFloat(itemPrice.base_amount) * item.quantity;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (usdTotal > 0) {
|
|
854
|
+
// Get exchange rate for this currency
|
|
855
|
+
const rateSymbol =
|
|
856
|
+
currency.method?.type === 'arcblock' ? 'ABT' : `${currency.symbol}@${currency.method?.type}`;
|
|
857
|
+
// eslint-disable-next-line no-await-in-loop
|
|
858
|
+
const rateResult = await exchangeRateService.getRate(rateSymbol);
|
|
859
|
+
const rate = parseFloat(rateResult.rate);
|
|
860
|
+
|
|
861
|
+
if (rate > 0) {
|
|
862
|
+
const tokenAmount = usdTotal / rate;
|
|
863
|
+
const tokenAmountInUnits = Math.floor(tokenAmount * 10 ** currency.decimal);
|
|
864
|
+
estimatedAmount = `≈ ${formatAmountForDisplay(tokenAmountInUnits.toString(), currency.decimal, currency.symbol)}`;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch (error) {
|
|
868
|
+
logger.warn('Failed to calculate estimated amount for currency', {
|
|
869
|
+
currencyId,
|
|
870
|
+
error: (error as Error).message,
|
|
871
|
+
});
|
|
872
|
+
estimatedAmount = '—';
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
options.push({
|
|
877
|
+
currency,
|
|
878
|
+
estimatedAmount,
|
|
879
|
+
isCurrentMethod,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Sort: current method first, then by symbol
|
|
884
|
+
options.sort((a, b) => {
|
|
885
|
+
if (a.isCurrentMethod) return -1;
|
|
886
|
+
if (b.isCurrentMethod) return 1;
|
|
887
|
+
return a.currency.symbol.localeCompare(b.currency.symbol);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
return c.json({ options });
|
|
891
|
+
} catch (err: any) {
|
|
892
|
+
logger.error('Failed to get payment options for invoice', { error: err, invoiceId: c.req.param('id') });
|
|
893
|
+
return c.json({ error: `Failed to get payment options: ${err.message}` }, 500);
|
|
765
894
|
}
|
|
766
895
|
});
|
|
767
896
|
|
|
768
|
-
|
|
897
|
+
app.get('/:id', authPortal, async (c) => {
|
|
769
898
|
try {
|
|
770
899
|
const doc = (await Invoice.findOne({
|
|
771
|
-
where: { id: req.
|
|
900
|
+
where: { id: c.req.param('id') },
|
|
772
901
|
include: [
|
|
773
902
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
774
903
|
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
@@ -791,7 +920,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
791
920
|
})) as TInvoiceExpanded | null;
|
|
792
921
|
|
|
793
922
|
if (doc) {
|
|
794
|
-
const shouldSync = req.query
|
|
923
|
+
const shouldSync = c.req.query('sync') === 'true' || !!c.req.query('forceSync');
|
|
795
924
|
// Sync Stripe invoice when sync=true query parameter is present
|
|
796
925
|
if (doc.metadata?.stripe_id && doc.status !== 'paid') {
|
|
797
926
|
// @ts-ignore
|
|
@@ -953,7 +1082,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
953
1082
|
const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id || doc.metadata.prev_invoice_id, {
|
|
954
1083
|
attributes: ['id', 'number', 'status', 'billing_reason'],
|
|
955
1084
|
});
|
|
956
|
-
return
|
|
1085
|
+
return c.json({
|
|
957
1086
|
...json,
|
|
958
1087
|
discountDetails,
|
|
959
1088
|
relatedInvoice,
|
|
@@ -963,7 +1092,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
963
1092
|
quotes,
|
|
964
1093
|
});
|
|
965
1094
|
}
|
|
966
|
-
return
|
|
1095
|
+
return c.json({
|
|
967
1096
|
...json,
|
|
968
1097
|
discountDetails,
|
|
969
1098
|
relatedCreditGrants,
|
|
@@ -972,146 +1101,25 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
972
1101
|
quotes,
|
|
973
1102
|
});
|
|
974
1103
|
}
|
|
975
|
-
return
|
|
1104
|
+
return c.json(null, 404);
|
|
976
1105
|
} catch (err) {
|
|
977
1106
|
logger.error(err);
|
|
978
|
-
return
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Get available payment options for an invoice with estimated prices
|
|
984
|
-
* Used when user wants to switch payment method after price change
|
|
985
|
-
*/
|
|
986
|
-
router.get('/:id/payment-options', authPortal, async (req, res) => {
|
|
987
|
-
try {
|
|
988
|
-
const invoice = await Invoice.findByPk(req.params.id, {
|
|
989
|
-
include: [
|
|
990
|
-
{ model: InvoiceItem, as: 'lines' },
|
|
991
|
-
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
992
|
-
],
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
if (!invoice) {
|
|
996
|
-
return res.status(404).json({ error: 'Invoice not found' });
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Get all available currencies for this invoice's products
|
|
1000
|
-
const invoiceItems = (invoice as any).lines || [];
|
|
1001
|
-
const priceIds = invoiceItems.map((item: any) => item.price_id).filter(Boolean);
|
|
1002
|
-
|
|
1003
|
-
// Get all unique product IDs from prices
|
|
1004
|
-
const prices = await Price.findAll({
|
|
1005
|
-
where: { id: priceIds },
|
|
1006
|
-
include: [{ model: PaymentCurrency, as: 'currencies' }],
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
// Collect all unique currencies
|
|
1010
|
-
const currencyMap = new Map<string, any>();
|
|
1011
|
-
const currentCurrencyId = invoice.currency_id;
|
|
1012
|
-
|
|
1013
|
-
// Add current currency first
|
|
1014
|
-
const { paymentCurrency } = invoice as any;
|
|
1015
|
-
if (paymentCurrency) {
|
|
1016
|
-
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
1017
|
-
currencyMap.set(paymentCurrency.id, {
|
|
1018
|
-
...paymentCurrency.toJSON(),
|
|
1019
|
-
method: paymentMethod?.toJSON(),
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// Add other available currencies from prices
|
|
1024
|
-
for (const price of prices) {
|
|
1025
|
-
const priceCurrencies = (price as any).currencies || [];
|
|
1026
|
-
for (const currency of priceCurrencies) {
|
|
1027
|
-
if (!currencyMap.has(currency.id)) {
|
|
1028
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1029
|
-
const paymentMethod = await PaymentMethod.findByPk(currency.payment_method_id);
|
|
1030
|
-
currencyMap.set(currency.id, {
|
|
1031
|
-
...currency.toJSON(),
|
|
1032
|
-
method: paymentMethod?.toJSON(),
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Calculate estimated amounts for each currency
|
|
1039
|
-
const exchangeRateService = getExchangeRateService();
|
|
1040
|
-
const options = [];
|
|
1041
|
-
|
|
1042
|
-
for (const [currencyId, currency] of currencyMap) {
|
|
1043
|
-
let estimatedAmount = '';
|
|
1044
|
-
const isCurrentMethod = currencyId === currentCurrencyId;
|
|
1045
|
-
|
|
1046
|
-
// For current method, use the invoice amount
|
|
1047
|
-
if (isCurrentMethod) {
|
|
1048
|
-
estimatedAmount = formatAmountForDisplay(invoice.amount_due, currency.decimal, currency.symbol);
|
|
1049
|
-
} else {
|
|
1050
|
-
// For other methods, calculate based on USD base amount and current exchange rate
|
|
1051
|
-
try {
|
|
1052
|
-
// Get USD total from line items
|
|
1053
|
-
let usdTotal = 0;
|
|
1054
|
-
for (const item of invoiceItems) {
|
|
1055
|
-
const itemPrice = prices.find((p) => p.id === item.price_id);
|
|
1056
|
-
if (itemPrice?.base_amount) {
|
|
1057
|
-
usdTotal += parseFloat(itemPrice.base_amount) * item.quantity;
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (usdTotal > 0) {
|
|
1062
|
-
// Get exchange rate for this currency
|
|
1063
|
-
const rateSymbol =
|
|
1064
|
-
currency.method?.type === 'arcblock' ? 'ABT' : `${currency.symbol}@${currency.method?.type}`;
|
|
1065
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1066
|
-
const rateResult = await exchangeRateService.getRate(rateSymbol);
|
|
1067
|
-
const rate = parseFloat(rateResult.rate);
|
|
1068
|
-
|
|
1069
|
-
if (rate > 0) {
|
|
1070
|
-
const tokenAmount = usdTotal / rate;
|
|
1071
|
-
const tokenAmountInUnits = Math.floor(tokenAmount * 10 ** currency.decimal);
|
|
1072
|
-
estimatedAmount = `≈ ${formatAmountForDisplay(tokenAmountInUnits.toString(), currency.decimal, currency.symbol)}`;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
logger.warn('Failed to calculate estimated amount for currency', {
|
|
1077
|
-
currencyId,
|
|
1078
|
-
error: (error as Error).message,
|
|
1079
|
-
});
|
|
1080
|
-
estimatedAmount = '—';
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
options.push({
|
|
1085
|
-
currency,
|
|
1086
|
-
estimatedAmount,
|
|
1087
|
-
isCurrentMethod,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// Sort: current method first, then by symbol
|
|
1092
|
-
options.sort((a, b) => {
|
|
1093
|
-
if (a.isCurrentMethod) return -1;
|
|
1094
|
-
if (b.isCurrentMethod) return 1;
|
|
1095
|
-
return a.currency.symbol.localeCompare(b.currency.symbol);
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
return res.json({ options });
|
|
1099
|
-
} catch (err: any) {
|
|
1100
|
-
logger.error('Failed to get payment options for invoice', { error: err, invoiceId: req.params.id });
|
|
1101
|
-
return res.status(500).json({ error: `Failed to get payment options: ${err.message}` });
|
|
1107
|
+
return c.json({ error: `Failed to get invoice: ${err.message}` }, 500);
|
|
1102
1108
|
}
|
|
1103
1109
|
});
|
|
1104
1110
|
|
|
1105
|
-
|
|
1111
|
+
// Static path — registered before PUT /:id so hono matches it first.
|
|
1112
|
+
app.post('/pay-stripe', authPortal, async (c) => {
|
|
1106
1113
|
try {
|
|
1107
|
-
const
|
|
1114
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
1115
|
+
const { invoice_ids, subscription_id, customer_id, currency_id } = body as any;
|
|
1108
1116
|
|
|
1109
1117
|
if (!currency_id) {
|
|
1110
|
-
return
|
|
1118
|
+
return c.json({ error: 'currency_id is required' }, 400);
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1113
1121
|
if (!invoice_ids && !subscription_id && !customer_id) {
|
|
1114
|
-
return
|
|
1122
|
+
return c.json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' }, 400);
|
|
1115
1123
|
}
|
|
1116
1124
|
|
|
1117
1125
|
let invoices: Invoice[];
|
|
@@ -1132,7 +1140,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1132
1140
|
});
|
|
1133
1141
|
|
|
1134
1142
|
if (invoices.length === 0) {
|
|
1135
|
-
return
|
|
1143
|
+
return c.json({ error: 'No payable invoices found' }, 404);
|
|
1136
1144
|
}
|
|
1137
1145
|
|
|
1138
1146
|
// @ts-ignore
|
|
@@ -1144,7 +1152,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1144
1152
|
});
|
|
1145
1153
|
|
|
1146
1154
|
if (!subscription) {
|
|
1147
|
-
return
|
|
1155
|
+
return c.json({ error: 'Subscription not found' }, 404);
|
|
1148
1156
|
}
|
|
1149
1157
|
|
|
1150
1158
|
// @ts-ignore
|
|
@@ -1165,7 +1173,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1165
1173
|
} else {
|
|
1166
1174
|
customer = await Customer.findByPkOrDid(customer_id!);
|
|
1167
1175
|
if (!customer) {
|
|
1168
|
-
return
|
|
1176
|
+
return c.json({ error: 'Customer not found' }, 404);
|
|
1169
1177
|
}
|
|
1170
1178
|
|
|
1171
1179
|
invoices = await Invoice.findAll({
|
|
@@ -1181,22 +1189,22 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1181
1189
|
});
|
|
1182
1190
|
|
|
1183
1191
|
if (invoices.length === 0) {
|
|
1184
|
-
return
|
|
1192
|
+
return c.json({ error: 'No payable invoices found' }, 404);
|
|
1185
1193
|
}
|
|
1186
1194
|
|
|
1187
1195
|
paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
|
|
1188
1196
|
}
|
|
1189
1197
|
|
|
1190
1198
|
if (!customer) {
|
|
1191
|
-
return
|
|
1199
|
+
return c.json({ error: 'Customer not found' }, 404);
|
|
1192
1200
|
}
|
|
1193
1201
|
|
|
1194
1202
|
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
1195
|
-
return
|
|
1203
|
+
return c.json({ error: 'Not using Stripe payment method' }, 400);
|
|
1196
1204
|
}
|
|
1197
1205
|
|
|
1198
1206
|
if (invoices.length === 0) {
|
|
1199
|
-
return
|
|
1207
|
+
return c.json({ error: 'No payable invoices found' }, 400);
|
|
1200
1208
|
}
|
|
1201
1209
|
|
|
1202
1210
|
await ensureStripeCustomer(customer, paymentMethod);
|
|
@@ -1205,7 +1213,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1205
1213
|
|
|
1206
1214
|
const paymentCurrency = await PaymentCurrency.findByPk(currency_id);
|
|
1207
1215
|
if (!paymentCurrency) {
|
|
1208
|
-
return
|
|
1216
|
+
return c.json({ error: `Payment currency ${currency_id} not found` }, 404);
|
|
1209
1217
|
}
|
|
1210
1218
|
const totalAmount = invoices.reduce((sum, invoice) => {
|
|
1211
1219
|
const amount = invoice.amount_remaining || '0';
|
|
@@ -1220,7 +1228,7 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1220
1228
|
|
|
1221
1229
|
const setupIntent = await ensureStripeSetupIntentForInvoicePayment(customer, paymentMethod, metadata);
|
|
1222
1230
|
|
|
1223
|
-
return
|
|
1231
|
+
return c.json({
|
|
1224
1232
|
client_secret: setupIntent.client_secret,
|
|
1225
1233
|
publishable_key: settings.stripe?.publishable_key,
|
|
1226
1234
|
setup_intent_id: setupIntent.id,
|
|
@@ -1232,53 +1240,54 @@ router.post('/pay-stripe', authPortal, async (req, res) => {
|
|
|
1232
1240
|
} catch (err) {
|
|
1233
1241
|
logger.error('Failed to create setup intent for stripe payment', {
|
|
1234
1242
|
error: err,
|
|
1235
|
-
body:
|
|
1243
|
+
body: c.get('sanitizedBody'),
|
|
1236
1244
|
});
|
|
1237
|
-
return
|
|
1245
|
+
return c.json({ error: err.message }, 400);
|
|
1238
1246
|
}
|
|
1239
1247
|
});
|
|
1240
1248
|
|
|
1241
1249
|
// eslint-disable-next-line consistent-return
|
|
1242
|
-
|
|
1250
|
+
app.put('/:id', authAdmin, async (c) => {
|
|
1243
1251
|
try {
|
|
1244
|
-
const doc = await Invoice.findByPk(req.
|
|
1252
|
+
const doc = await Invoice.findByPk(c.req.param('id') as string);
|
|
1245
1253
|
if (!doc) {
|
|
1246
|
-
return
|
|
1254
|
+
return c.json({ error: 'Invoice not found' }, 404);
|
|
1247
1255
|
}
|
|
1248
1256
|
|
|
1249
|
-
const
|
|
1257
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
1258
|
+
const raw = pick(body, ['metadata']);
|
|
1250
1259
|
if (raw.metadata) {
|
|
1251
1260
|
const { error } = MetadataSchema.validate(raw.metadata);
|
|
1252
1261
|
if (error) {
|
|
1253
|
-
return
|
|
1262
|
+
return c.json({ error: error.message }, 400);
|
|
1254
1263
|
}
|
|
1255
1264
|
raw.metadata = formatMetadata(raw.metadata);
|
|
1256
1265
|
}
|
|
1257
1266
|
|
|
1258
1267
|
await doc.update(raw);
|
|
1259
|
-
|
|
1268
|
+
return c.json(doc);
|
|
1260
1269
|
} catch (err) {
|
|
1261
1270
|
logger.error(err);
|
|
1262
|
-
|
|
1271
|
+
return c.json(null);
|
|
1263
1272
|
}
|
|
1264
1273
|
});
|
|
1265
1274
|
|
|
1266
|
-
|
|
1267
|
-
const invoice = await Invoice.findByPk(req.
|
|
1275
|
+
app.post('/:id/void', authAdmin, async (c) => {
|
|
1276
|
+
const invoice = await Invoice.findByPk(c.req.param('id') as string);
|
|
1268
1277
|
if (!invoice) {
|
|
1269
|
-
return
|
|
1278
|
+
return c.json({ error: 'Invoice not found' }, 404);
|
|
1270
1279
|
}
|
|
1271
1280
|
if (['paid', 'void', 'draft'].includes(invoice.status)) {
|
|
1272
|
-
return
|
|
1281
|
+
return c.json({ error: 'Can not void this invoice' }, 400);
|
|
1273
1282
|
}
|
|
1274
1283
|
const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
1275
1284
|
if (!paymentMethod) {
|
|
1276
|
-
return
|
|
1285
|
+
return c.json({ error: 'Payment method not found' }, 400);
|
|
1277
1286
|
}
|
|
1278
1287
|
if (invoice.subscription_id) {
|
|
1279
1288
|
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
1280
1289
|
if (subscription && !subscription.isImmutable()) {
|
|
1281
|
-
return
|
|
1290
|
+
return c.json({ error: 'Subscription is not immutable, can not void invoice' }, 400);
|
|
1282
1291
|
}
|
|
1283
1292
|
}
|
|
1284
1293
|
try {
|
|
@@ -1303,11 +1312,11 @@ router.post('/:id/void', authAdmin, async (req, res) => {
|
|
|
1303
1312
|
voided_at: dayjs().unix(),
|
|
1304
1313
|
},
|
|
1305
1314
|
});
|
|
1306
|
-
return
|
|
1315
|
+
return c.json(invoice);
|
|
1307
1316
|
} catch (error) {
|
|
1308
1317
|
logger.error('Failed to void invoice', { error, invoiceId: invoice.id });
|
|
1309
|
-
return
|
|
1318
|
+
return c.json({ error: 'Failed to void invoice' }, 400);
|
|
1310
1319
|
}
|
|
1311
1320
|
});
|
|
1312
1321
|
|
|
1313
|
-
export default
|
|
1322
|
+
export default app;
|