Payment
Source: src/data/payment/
Service
createPaymentService / PaymentService
Factory: createPaymentService(repository: PaymentRepository, events?: PaymentEventEmitter) — returns PaymentService.
Provides payment lifecycle operations for bot order flow and P2P admin approval.
| Method | Parameters | Returns | Description |
|---|---|---|---|
| createPending | (ctx, input: { orderId, method, amountCents, currency, referenceText? }) | Promise<PaymentRecord> | Creates a pending payment; idempotent for same order id |
| confirmByOrder | (ctx, input: { orderId, confirmedBy, providerRef? }) | Promise<PaymentRecord> | Legacy: confirms by order id, marks PAID |
| getByOrder | (ctx, orderId) | Promise<PaymentRecord | null> | Reads payment by order |
| submitForApproval | (ctx, paymentId) | Promise<PaymentRecord> | Transitions pending_payment → awaiting_admin_approval. Idempotent. |
| confirmPayment | (ctx, paymentId, approverUserId, referenceNote?) | Promise<PaymentRecord> | Transitions awaiting_admin_approval → confirmed. Emits payment.confirmed. Idempotent. |
| rejectPayment | (ctx, paymentId, approverUserId, reason) | Promise<PaymentRecord> | Transitions awaiting_admin_approval → rejected. Emits payment.rejected. Idempotent. |
PaymentEventEmitter
type PaymentEventEmitter = { emit: (event: string, payload: unknown) => void }
Passed as optional second arg to createPaymentService. When provided, payment.confirmed and payment.rejected are emitted on the respective transitions.
Repository
PaymentRepository
| Method | Parameters | Returns |
|---|---|---|
| getById | (ctx, id) | Promise<PaymentRecord | null> |
| findByOrderId | (ctx, orderId) | Promise<PaymentRecord | null> |
| listByTenant | (ctx) | Promise<PaymentRecord[]> |
| create | (ctx, input) | Promise<PaymentRecord> |
| update | (ctx, id, patch) | Promise<PaymentRecord> |
| delete | (ctx, id) | Promise<void> |
PaymentRecord
| Field | Type | Description |
|---|---|---|
| id | string | |
| tenantId | string | |
| orderId | string | |
| method | PaymentMethod | |
| status | PaymentStatus | |
| amountCents | number | Original order total in cents |
| refundedAmountCents | number | Running total of all refunded amounts (default 0). 0 < n < amountCents → PARTIAL_REFUND; n = amountCents → REFUNDED. |
| currency | string | |
| providerRef | string (optional) | |
| referenceText | string (optional) | |
| confirmedBy | string (optional) | Legacy field |
| confirmedAt | Date (optional) | Legacy field |
| approverUserId | string (optional) | Admin who confirmed or rejected |
| approvedAt | Date (optional) | When confirmed |
| rejectedAt | Date (optional) | When rejected |
| rejectionReason | string (optional) | Reason for rejection |
| customerReferenceNote | string (optional) | Venmo/CashApp note admin saw |
| createdAt | Date | |
| updatedAt | Date |
PaymentMethod
One of: PAYPAL, CASHAPP_PAY, BTC_INVOICE, VENMO_P2P, CASHAPP_P2P, ZELLE, CHIME, PAYPAL_MANUAL, BTC_MANUAL, BTC_NOWPAYMENTS
Each value maps to a PaymentMethodDescriptor in src/data/payment-methods/payment-methods.registry.ts. The descriptor carries displayName, kind (p2p | processor | manual), icon, and flags for requiresProcessor, supportsAutomaticConfirmation, and supportsRefund. Use getPaymentMethodDescriptor(method) or getEnabledDescriptors(enabledMethods) from payment-methods.service.ts to look up human-readable labels instead of formatting raw enum strings.
PaymentStatus
Legacy values: PENDING, AUTHORIZED, PAID, FAILED, REFUNDED, PARTIAL_REFUND, CANCELLED
P2P approval flow values: pending_payment, awaiting_admin_approval, confirmed, rejected
PARTIAL_REFUND is set automatically when refundedAmountCents > 0 and refundedAmountCents < amountCents after a partial refund. It transitions to REFUNDED once refundedAmountCents = amountCents.
Events
payment.confirmed
Emitted when confirmPayment transitions a payment to confirmed.
type PaymentConfirmedEvent = {
tenantId: string
paymentId: string
orderId: string
approverUserId: string
referenceNote?: string
confirmedAt: Date
}
payment.rejected
Emitted when rejectPayment transitions a payment to rejected.
type PaymentRejectedEvent = {
tenantId: string
paymentId: string
orderId: string
approverUserId: string
reason: string
rejectedAt: Date
}
PaymentNowPaymentsService
Source: src/data/payment/payment-nowpayments.service.ts
Factory: createPaymentNowPaymentsService(deps) — returns PaymentNowPaymentsService.
Bridges the CryptoPaymentPort (NOWPayments adapter) with the Payment repository to create BTC invoices for the BTC_NOWPAYMENTS checkout flow. Only instantiated when NOWPAYMENTS_API_KEY is configured; otherwise undefined at all call sites.
| Method | Parameters | Returns | Description |
|---|---|---|---|
| createInvoice | (ctx, { paymentId, amountCents, currency, customerEmail?, callbackUrl? }) | Promise<NowPaymentsInvoiceResult> | Creates a NOWPayments hosted invoice (BTC). On success, persists providerRef + five nowpayments* fields to the Payment record. Persist failure logs a warning but still returns the invoice result. |
| getInvoiceStatus | (invoiceId) | Promise<NowPaymentsStatus> | Fetches current status from NOWPayments and maps to 'pending' | 'finished' | 'failed' | 'expired'. |
NowPaymentsInvoiceResult
| Field | Type | Description |
|---|---|---|
| invoiceId | string | NOWPayments invoice ID |
| hostedUrl | string | Customer-facing hosted payment page URL |
| depositAddress | string | On-chain BTC deposit address |
| depositAmountCrypto | string | Expected BTC amount (decimal string) |
| cryptoCurrency | string | Always 'btc' for this service |
| expiresAt | Date (optional) | Invoice expiry timestamp |
NowPaymentsStatus
One of: 'pending', 'finished', 'failed', 'expired'
Maps from the CryptoPaymentPort status values: pending → pending, partially_paid → pending, finished → finished, failed → failed, expired → expired. Unknown values fall back to 'pending'.
Notification helper
buildApprovalNotification
Source: src/data/payment/payment.notification.ts
Builds the Discord embed + button row for a payment awaiting admin approval.
buildApprovalNotification(order, payment, customer): { embeds, components }
Used by the Notifications agent to post the approval embed to the admin channel when an order with a P2P payment is created.
Processor-backed vs manual-only methods
The pay-by-invoice page (GET /orders/invoice/:token) displays only processor-backed methods — methods that can be completed by the customer without staff intervention.
Excluded (manual-only): VENMO_P2P, CASHAPP_P2P, ZELLE, CHIME, PAYPAL_MANUAL, BTC_MANUAL
Included (processor-backed): PAYPAL, CASHAPP_PAY, BTC_INVOICE, BTC_NOWPAYMENTS, and any future processor methods.
The filter is applied in order-invoice.endpoint.ts using the MANUAL_ONLY_METHODS set.
Validators
See src/data/payment/payment.validators.ts for the complete Zod schema. New optional fields: approverUserId, approvedAt, rejectedAt, rejectionReason, customerReferenceNote.