UNPKG

@sahabaplus/moyasar

Version:

A comprehensive TypeScript SDK for integrating with the Moyasar payment gateway

1 lines 147 kB
{"version":3,"file":"index.cjs","sources":["../src/shared/constants/api.constants.ts","../src/shared/errors/moyasar_error.ts","../src/client/base-client.ts","../src/features/webhook/enums.ts","../src/features/webhook/validation.ts","../src/features/webhook/errors.ts","../src/features/webhook/utils.ts","../src/features/payment/enums.ts","../src/features/payment/errors.ts","../src/features/payment/constants.ts","../src/shared/validation/amount_schema.ts","../src/shared/types/currency.ts","../src/shared/validation/currency_schema.ts","../src/shared/validation/pagination_meta_schema.ts","../src/features/payment/validation/schemas.ts","../src/features/payment/utils.ts","../src/features/payment/service.ts","../src/features/webhook/service.ts","../src/features/invoice/enums.ts","../src/features/invoice/errors.ts","../src/features/invoice/constants.ts","../src/features/invoice/validation/schemas.ts","../src/features/invoice/utils.ts","../src/features/invoice/service.ts","../src/client/moyasar-client.ts"],"sourcesContent":["export const API_ENDPOINTS = {\n webhooks: \"/v1/webhooks\",\n webhookAttempts: \"/v1/webhooks/attempts\",\n availableEvents: \"/v1/webhooks/available_events\",\n invoices: \"/v1/invoices\",\n bulkInvoices: \"/v1/invoices/bulk\",\n payments: \"/v1/payments\",\n bulkPayments: \"/v1/payments/bulk\",\n} as const;\n\nexport const DEFAULT_API_CONFIG = {\n BASE_URL: \"https://api.moyasar.com\",\n TIMEOUT: 30000,\n} as const;\n","export class MoyasarError extends Error {\n public readonly type: string;\n public readonly statusCode: number;\n public readonly details: Record<string, any>;\n\n constructor(\n message: string,\n type: string = \"MOYASAR_ERROR\",\n statusCode: number,\n details: Record<string, any>\n ) {\n super(message);\n this.name = \"MoyasarError\";\n this.type = type;\n this.statusCode = statusCode;\n this.details = details;\n }\n}\n","import axios, {\n AxiosError,\n type AxiosInstance,\n type AxiosRequestConfig,\n type AxiosResponse,\n} from \"axios\";\nimport { type ApiClientOptions, type RequestConfig } from \"@types\";\nimport { DEFAULT_API_CONFIG } from \"@constants\";\nimport { MoyasarError } from \"@errors\";\n\nexport abstract class BaseAxiosApiClient {\n private axiosInstance: AxiosInstance;\n\n constructor(apiKey: string, options: ApiClientOptions = {}) {\n // Create axios instance with base configuration\n this.axiosInstance = axios.create({\n baseURL: options.baseUrl || DEFAULT_API_CONFIG.BASE_URL,\n timeout: options.timeout || DEFAULT_API_CONFIG.TIMEOUT,\n headers: {\n Authorization: `Basic ${Buffer.from(apiKey + \":\").toString(\"base64\")}`,\n \"Content-Type\": \"application/json\",\n \"User-Agent\": \"Moyasar-SDK/1.0.0\",\n },\n });\n\n // Setup response interceptor for error handling\n this.setupInterceptors();\n }\n\n private setupInterceptors(): void {\n // Response interceptor to handle errors\n this.axiosInstance.interceptors.response.use(\n (response: AxiosResponse) => response,\n (error: AxiosError) => {\n throw this.createErrorFromAxiosError(error);\n }\n );\n }\n\n async request<T = any>(config: RequestConfig): Promise<T> {\n const response = await this.axiosInstance.request<T>(config);\n return response.data;\n }\n\n private createErrorFromAxiosError(error: AxiosError): MoyasarError {\n const response = error.response;\n\n let errorData: {\n type?: string;\n message?: string;\n errors?: string[];\n } = {};\n let message = error.message;\n let statusCode = response?.status ?? 500;\n\n // Handle network errors\n if (!response) {\n return new MoyasarError(\n error.message || \"Network error occurred\",\n \"api_connection_error\",\n statusCode,\n {\n code: error.code,\n originalError: error.message,\n }\n );\n }\n\n statusCode = response.status;\n // Parse error response data\n if (response.data) errorData = response.data;\n\n // Determine error type and message based on Moyasar API documentation\n let errorType = \"api_error\";\n\n if (errorData.type) errorType = errorData.type;\n else errorType = this.getDefaultErrorType(statusCode, errorType);\n\n // Use error message from response or generate appropriate message\n if (errorData.message) message = errorData.message;\n else message = this.getDefaultErrorMessage(statusCode, errorType);\n\n const moyasarError = new MoyasarError(message, errorType, statusCode, {\n url: error.config?.url,\n status: statusCode,\n statusText: response.statusText,\n errors: errorData.errors, // Detailed validation errors\n ...errorData,\n });\n\n return moyasarError;\n }\n\n private getDefaultErrorType(statusCode: number, errorType: string) {\n switch (statusCode) {\n case 400:\n errorType = \"invalid_request_error\";\n break;\n case 401:\n errorType = \"authentication_error\";\n break;\n case 403:\n errorType = \"account_inactive_error\";\n break;\n case 404:\n errorType = \"invalid_request_error\";\n break;\n case 405:\n errorType = \"account_inactive_error\";\n break;\n case 429:\n errorType = \"rate_limit_error\";\n break;\n case 500:\n case 503:\n errorType = \"api_error\";\n break;\n default:\n errorType = \"api_error\";\n }\n return errorType;\n }\n\n private getDefaultErrorMessage(\n statusCode: number,\n errorType: string\n ): string {\n switch (statusCode) {\n case 400:\n return \"The request was unacceptable, often due to missing a required parameter\";\n case 401:\n return \"Invalid authorization credentials\";\n case 403:\n return \"Credentials not enough to access resources\";\n case 404:\n return \"The requested resource doesn't exist\";\n case 405:\n return \"Entity not activated to use live account\";\n case 429:\n return \"Too many requests hit the API too quickly\";\n case 500:\n return \"We had a problem with our server. Try again later\";\n case 503:\n return \"We are temporarily offline for maintenance. Please try again later\";\n default:\n return `HTTP ${statusCode}: Request failed`;\n }\n }\n\n // Getter for base URL (maintaining API compatibility)\n get baseUrl(): string {\n return this.axiosInstance.defaults.baseURL || \"\";\n }\n\n // Setter for base URL (maintaining API compatibility)\n set baseUrl(url: string) {\n this.axiosInstance.defaults.baseURL = url;\n }\n}\n","export const WebhookEvent = {\n /**\n * ## Initiated\n * First status of a payment. It indicates that the payment has been created but the cardholder did not pay yet.\n * @hint If the payment is initiated then you need to complete the payment transaction by completing the payment challenge found in `Payment.source.transaction_url` depending on the payment source type.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=initiated\n */\n PAYOUT_INITIATED: \"payout_initiated\",\n /**\n * ## Payment Paid\n * Payment reaches this status when the cardholder pays successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=paid\n */\n PAYMENT_PAID: \"payment_paid\",\n /**\n * ## Payment Failed\n * Payment reaches this status when the cardholder or merchant has a certain error that caused the payment to fail (errors are attached to the `message` attribute).\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=failed\n */\n PAYMENT_FAILED: \"payment_failed\",\n /**\n * ## Payment Authorized\n * Payment reaches this status when the merchant authorizes it to be manually captured anytime later —the cardholder is not charged yet.\n * @note The status authorized is used when a scheme payment is made with `manual: true` option which will cause the system to authorize the payment only without capturing it. The merchant must capture the payment within time it will be voided automatically by the issuer.\n * @note Please note that when an issuer voids the payment, the status will be kept authorized and **WILL NOT BE updated by the system**.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=authorized\n * @see https://docs.moyasar.com/api/payments/01-create-payment#responses\n */\n PAYMENT_AUTHORIZED: \"payment_authorized\",\n /**\n * ## Payment Captured\n * Payment reaches this status when the cardholder of an authorized payment is charged successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=captured\n */\n PAYMENT_CAPTURED: \"payment_captured\",\n /**\n * ## Payment Refunded\n * Payment reaches this status when the merchant refunds a paid or captured payment successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=refunded\n */\n PAYMENT_REFUNDED: \"payment_refunded\",\n /**\n * ## Payment Voided\n * Payment reaches this status when the merchant cancels a paid, authorized, or captured payment. It works only if the amount is not settled yet in the merchant’s bank account.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=voided\n */\n PAYMENT_VOIDED: \"payment_voided\",\n /**\n * ## Payment Verified\n * Payment reaches this status when the cardholder verifies his card in the tokenization process.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=verified\n */\n PAYMENT_VERIFIED: \"payment_verified\",\n PAYMENT_ABANDONED: \"payment_abandoned\",\n PAYMENT_CANCELED: \"payment_canceled\",\n PAYMENT_EXPIRED: \"payment_expired\",\n /**\n * ## Balance Transferred\n * When a settlement is created and sent to the merchant bank account, Moyasar will send a webhook notification to the merchant\n * @see https://docs.moyasar.com/guides/settlements/settlement-notification/#:~:text=Make%20sure%20to%20selected%20the%20balance_transferred%20event\n */\n BALANCE_TRANSFERRED: \"balance_transferred\",\n PAYOUT_PAID: \"payout_paid\",\n PAYOUT_FAILED: \"payout_failed\",\n PAYOUT_CANCELED: \"payout_canceled\",\n PAYOUT_RETURNED: \"payout_returned\",\n} as const;\n\nexport type WebhookEvent = (typeof WebhookEvent)[keyof typeof WebhookEvent];\n\nexport const ALL_WEBHOOK_EVENTS = Object.values(WebhookEvent);\n\nexport const WebhookHttpMethod = {\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n} as const;\n\nexport type WebhookHttpMethod =\n (typeof WebhookHttpMethod)[keyof typeof WebhookHttpMethod];\n","import { WebhookEvent, WebhookHttpMethod } from \"./enums\";\n\nexport class WebhookValidation {\n /**\n * Validate webhook event type\n */\n static isValidWebhookEvent(event: string): boolean {\n return Object.values(WebhookEvent).includes(event as any);\n }\n\n /**\n * Validate HTTP method\n */\n static isValidHttpMethod(method: string): boolean {\n return Object.values(WebhookHttpMethod).includes(method as any);\n }\n\n /**\n * Validate URL format\n */\n static isValidUrl(url: string): boolean {\n try {\n const parsedUrl = new URL(url);\n return parsedUrl.protocol === \"https:\"; // Only allow HTTPS\n } catch {\n return false;\n }\n }\n\n /**\n * Validate required fields\n */\n static validateRequired<T extends Record<string, any>>(\n obj: T,\n requiredFields: (keyof T)[]\n ): string[] {\n const errors: string[] = [];\n\n for (const field of requiredFields) {\n if (\n obj[field] === undefined ||\n obj[field] === null ||\n obj[field] === \"\"\n ) {\n errors.push(`${String(field)} is required`);\n }\n }\n\n return errors;\n }\n\n /**\n * Sanitize webhook URL\n */\n static sanitizeUrl(url: string): string {\n return url.trim().toLowerCase();\n }\n}\n","import { MoyasarError } from \"@errors\";\nimport type { WebhookPayload } from \"./types\";\n\nexport class WebhookError extends MoyasarError {\n constructor(message: string, details?: Record<string, any>) {\n super(message, \"WEBHOOK_ERROR\", 500, details ?? {});\n this.name = \"WebhookError\";\n }\n}\n\nexport class WebhookVerificationError extends WebhookError {\n constructor(message: string = \"Webhook signature verification failed\") {\n super(message, {});\n this.name = \"WebhookVerificationError\";\n }\n}\n\nexport class WebhookValidationError extends WebhookError {\n public readonly unexpected_payload: unknown;\n constructor({\n message = \"Webhook payload validation failed\",\n unexpected_payload,\n }: {\n message?: string;\n unexpected_payload: unknown;\n }) {\n super(message, { unexpected_payload });\n this.unexpected_payload = unexpected_payload;\n this.name = \"WebhookValidationError\";\n }\n}\n","import { WebhookValidation } from \"./validation\";\nimport { WebhookError, WebhookValidationError } from \"./errors\";\nimport type { WebhookPayload } from \"./types\";\n\nexport class WebhookUtils {\n /**\n * Verify webhook payload signature\n */\n static async verifyWebhookSignature(\n payload: { secret_token: string },\n options: { secret_token: string }\n ): Promise<boolean> {\n const { secret_token } = options;\n\n return secret_token === payload.secret_token;\n }\n\n /**\n * Parse webhook payload safely\n */\n static parseWebhookPayload(rawPayload: string | Buffer): WebhookPayload {\n try {\n const payloadString =\n rawPayload instanceof Buffer\n ? rawPayload.toString(\"utf8\")\n : (rawPayload as string);\n\n const parsed = JSON.parse(payloadString);\n\n // Basic validation\n if (!parsed.id || !parsed.type || !parsed.data)\n throw new WebhookValidationError({\n message: \"Invalid webhook payload structure\",\n unexpected_payload: parsed,\n });\n\n return parsed as WebhookPayload;\n } catch (error) {\n if (error instanceof WebhookError) {\n throw error;\n }\n throw new WebhookValidationError({\n message: `Failed to parse webhook payload: ${error}`,\n unexpected_payload: {},\n });\n }\n }\n\n /**\n * Validate webhook payload\n */\n static validateWebhookPayload(payload: unknown): string[] {\n const errors: string[] = [];\n\n if (typeof payload !== \"object\" || payload === null) {\n errors.push(\n \"Invalid webhook payload, expected an object got \" + typeof payload\n );\n return errors;\n }\n\n // Check required fields\n errors.push(\n ...WebhookValidation.validateRequired(payload as Record<string, any>, [\n \"id\",\n \"type\",\n \"created_at\",\n \"account_name\",\n \"data\",\n ])\n );\n\n // Validate event type\n if (!(\"type\" in payload) || typeof payload.type !== \"string\") {\n errors.push(`Invalid webhook event type`);\n } else if (!WebhookValidation.isValidWebhookEvent(payload.type)) {\n errors.push(\n `Invalid webhook event type: ${JSON.stringify(payload.type)}`\n );\n }\n\n // Validate live mode\n if (!(\"live\" in payload) || typeof payload.live !== \"boolean\") {\n errors.push(\"live field must be a boolean\");\n }\n\n return errors;\n }\n\n /**\n * Extract signature from headers (common patterns)\n */\n static extractSignatureFromHeaders(\n headers: Record<string, string | string[]>\n ): string | null {\n // Common signature header patterns\n const signatureHeaders = [\n \"x-moyasar-signature\",\n \"x-signature\",\n \"signature\",\n \"authorization\",\n ];\n\n for (const header of signatureHeaders) {\n const value = headers[header] || headers[header.toLowerCase()];\n if (value) {\n const signature = Array.isArray(value) ? value[0]! : value;\n // Remove common prefixes\n return signature.replace(/^(sha256=|hmac-sha256=|Bearer\\s+)/i, \"\");\n }\n }\n\n return null;\n }\n\n /**\n * Create a consistent payload string for signature verification\n */\n static createSignaturePayload(\n payload: WebhookPayload,\n timestamp?: number\n ): string {\n const basePayload = JSON.stringify(payload);\n return timestamp ? `${timestamp}.${basePayload}` : basePayload;\n }\n\n /**\n * Get webhook event categories\n */\n static getEventCategory(event: string): \"payment\" | \"unknown\" {\n if (event.startsWith(\"payment_\")) {\n return \"payment\";\n }\n return \"unknown\";\n }\n\n /**\n * Check if webhook should be retried based on response\n */\n static shouldRetryWebhook(\n statusCode: number,\n retryCount: number,\n maxRetries: number = 5\n ): boolean {\n if (retryCount >= maxRetries) {\n return false;\n }\n\n // Don't retry client errors (4xx) except specific ones\n if (statusCode >= 400 && statusCode < 500) {\n return [408, 429].includes(statusCode); // Timeout, Rate Limited\n }\n\n // Retry server errors (5xx) and network errors\n return statusCode >= 500 || statusCode === 0;\n }\n}\n","/**\n * @description Indicates the payment status. If the payment is in the initiated status, then an action must be taken (e.g. 3DS challenge) in order to complete the payment. The status authorized is used when a scheme payment is made with manual: true option which will cause the system to authorize the payment only without capturing it. The merchant must capture the payment within time it will be voided automatically by the issuer.\n * @note Please note that when an issuer voids the payment, the status will be kept authorized and WILL NOT BE updated by the system.\n * @values `initiated`, `paid`, `authorized`, `failed`, `refunded`, `captured`, `voided`, `verified`\n * @see https://docs.moyasar.com/api/payments/payment-status-reference\n */\nexport const PaymentStatus = {\n /**\n * ## Initiated\n * First status of a payment. It indicates that the payment has been created but the cardholder did not pay yet.\n * @hint If the payment is initiated then you need to complete the payment transaction by completing the payment challenge found in `Payment.source.transaction_url` depending on the payment source type.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=initiated\n */\n INITIATED: \"initiated\",\n /**\n * ## Paid\n * Payment reaches this status when the cardholder pays successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=paid\n */\n PAID: \"paid\",\n /**\n * ## Failed\n * Payment reaches this status when the cardholder or merchant has a certain error that caused the payment to fail (errors are attached to the `message` attribute).\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=failed\n */\n FAILED: \"failed\",\n /**\n * ## Authorized\n * Payment reaches this status when the merchant authorizes it to be manually captured anytime later —the cardholder is not charged yet.\n * @note The status authorized is used when a scheme payment is made with `manual: true` option which will cause the system to authorize the payment only without capturing it. The merchant must capture the payment within time it will be voided automatically by the issuer.\n * @note Please note that when an issuer voids the payment, the status will be kept authorized and **WILL NOT BE updated by the system**.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=authorized\n * @see https://docs.moyasar.com/api/payments/01-create-payment#responses\n */\n AUTHORIZED: \"authorized\",\n /**\n * ## Captured\n * Payment reaches this status when the cardholder of an authorized payment is charged successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=captured\n */\n CAPTURED: \"captured\",\n /**\n * ## Refunded\n * Payment reaches this status when the merchant refunds a paid or captured payment successfully.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=refunded\n */\n REFUNDED: \"refunded\",\n /**\n * ## Voided\n * Payment reaches this status when the merchant cancels a paid, authorized, or captured payment. It works only if the amount is not settled yet in the merchant’s bank account.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=voided\n */\n VOIDED: \"voided\",\n /**\n * ## Verified\n * Payment reaches this status when the cardholder verifies his card in the tokenization process.\n * @see https://docs.moyasar.com/api/payments/payment-status-reference?_highlight=verified\n */\n VERIFIED: \"verified\",\n} as const;\n\nexport type PaymentStatus = (typeof PaymentStatus)[keyof typeof PaymentStatus];\n\nexport const PaymentSource = {\n CREDITCARD: \"creditcard\",\n APPLEPAY: \"applepay\",\n GOOGLEPAY: \"googlepay\",\n SAMSUNGPAY: \"samsungpay\",\n STCPAY: \"stcpay\",\n TOKEN: \"token\",\n} as const;\n\nexport type PaymentSource = (typeof PaymentSource)[keyof typeof PaymentSource];\n\nexport const CardScheme = {\n MADA: \"mada\",\n VISA: \"visa\",\n MASTER: \"master\",\n AMEX: \"amex\",\n} as const;\n\nexport type CardScheme = (typeof CardScheme)[keyof typeof CardScheme];\n\nexport const CardType = {\n DEBIT: \"debit\",\n CREDIT: \"credit\",\n CHARGE_CARD: \"charge_card\",\n UNSPECIFIED: \"unspecified\",\n} as const;\n\nexport type CardType = (typeof CardType)[keyof typeof CardType];\n","import { MoyasarError } from \"@errors\";\n\nexport class PaymentError extends MoyasarError {\n constructor(\n message: string,\n statusCode: number,\n details?: Record<string, any>\n ) {\n super(message, \"PAYMENT_ERROR\", statusCode, details ?? {});\n this.name = \"PaymentError\";\n }\n}\n","export const PaymentLimits = {\n MIN_AMOUNT: 50, // Minimum payment amount in smallest currency unit\n MAX_AMOUNT: 100000000, // Maximum payment amount (100M in smallest unit)\n REFUND_TIMEOUT_DAYS: 30, // Days after which refunds are not allowed\n CAPTURE_TIMEOUT_DAYS: 7, // Days after which authorized payments are auto-voided\n} as const;\n\nexport const PaymentValidation = {\n DESCRIPTION_MAX_LENGTH: 255,\n STATEMENT_DESCRIPTOR_MAX_LENGTH: 255,\n CARD_NUMBER_MIN_LENGTH: 16,\n CARD_NUMBER_MAX_LENGTH: 19,\n CVV_LENGTH: 3,\n AMEX_CVV_LENGTH: 4,\n SAUDI_MOBILE_REGEX: /^(0|(00|\\+)?966)?(5\\d{8})$/,\n RRN_REGEX: /^\\d{12}$/,\n AUTH_CODE_REGEX: /^\\d{6}$/,\n CARD_LAST_DIGITS_REGEX: /^\\d{4}$/,\n} as const;\n","import { z } from \"zod\";\n\nexport const amountSchema = z.number().int().positive().min(1);\n","export const Currency = Object.freeze({\n USD: 'USD',\n EUR: 'EUR',\n GBP: 'GBP',\n AUD: 'AUD',\n CAD: 'CAD',\n JPY: 'JPY',\n BYR: 'BYR',\n PAB: 'PAB',\n SOS: 'SOS',\n SRD: 'SRD',\n SSP: 'SSP',\n STD: 'STD',\n STN: 'STN',\n SVC: 'SVC',\n SYP: 'SYP',\n SZL: 'SZL',\n THB: 'THB',\n TJS: 'TJS',\n TMT: 'TMT',\n TND: 'TND',\n TOP: 'TOP',\n TRY: 'TRY',\n NPR: 'NPR',\n TTD: 'TTD',\n TWD: 'TWD',\n NOK: 'NOK',\n TZS: 'TZS',\n UAH: 'UAH',\n UGX: 'UGX',\n NIO: 'NIO',\n PEN: 'PEN',\n OMR: 'OMR',\n PGK: 'PGK',\n PHP: 'PHP',\n PKR: 'PKR',\n PLN: 'PLN',\n PYG: 'PYG',\n QAR: 'QAR',\n RON: 'RON',\n RSD: 'RSD',\n RUB: 'RUB',\n RWF: 'RWF',\n SAR: 'SAR',\n SBD: 'SBD',\n SCR: 'SCR',\n SDG: 'SDG',\n SEK: 'SEK',\n SGD: 'SGD',\n SHP: 'SHP',\n NZD: 'NZD',\n SKK: 'SKK',\n SLE: 'SLE',\n SLL: 'SLL',\n UYU: 'UYU',\n BCH: 'BCH',\n BTC: 'BTC',\n JEP: 'JEP',\n GGP: 'GGP',\n IMP: 'IMP',\n XFU: 'XFU',\n GBX: 'GBX',\n CNH: 'CNH',\n USDC: 'USDC',\n EEK: 'EEK',\n GHS: 'GHS',\n HRK: 'HRK',\n LTL: 'LTL',\n LVL: 'LVL',\n MRO: 'MRO',\n MTL: 'MTL',\n TMM: 'TMM',\n ZWD: 'ZWD',\n ZWL: 'ZWL',\n ZWN: 'ZWN',\n ZWR: 'ZWR',\n VEF: 'VEF',\n UZS: 'UZS',\n VES: 'VES',\n VND: 'VND',\n VUV: 'VUV',\n WST: 'WST',\n XAF: 'XAF',\n XAG: 'XAG',\n XAU: 'XAU',\n XBA: 'XBA',\n XBB: 'XBB',\n XBC: 'XBC',\n XBD: 'XBD',\n XCD: 'XCD',\n XDR: 'XDR',\n XOF: 'XOF',\n XPD: 'XPD',\n XPF: 'XPF',\n XPT: 'XPT',\n XTS: 'XTS',\n YER: 'YER',\n ZAR: 'ZAR',\n ZMK: 'ZMK',\n ZMW: 'ZMW',\n AED: 'AED',\n CDF: 'CDF',\n CHF: 'CHF',\n CLF: 'CLF',\n CLP: 'CLP',\n CNY: 'CNY',\n COP: 'COP',\n CRC: 'CRC',\n CUC: 'CUC',\n CUP: 'CUP',\n CVE: 'CVE',\n CZK: 'CZK',\n DJF: 'DJF',\n DKK: 'DKK',\n DOP: 'DOP',\n DZD: 'DZD',\n EGP: 'EGP',\n ERN: 'ERN',\n ETB: 'ETB',\n FJD: 'FJD',\n FKP: 'FKP',\n GEL: 'GEL',\n GIP: 'GIP',\n AFN: 'AFN',\n ALL: 'ALL',\n AMD: 'AMD',\n ANG: 'ANG',\n AOA: 'AOA',\n ARS: 'ARS',\n AWG: 'AWG',\n AZN: 'AZN',\n BAM: 'BAM',\n BBD: 'BBD',\n BDT: 'BDT',\n BGN: 'BGN',\n BHD: 'BHD',\n BIF: 'BIF',\n BMD: 'BMD',\n BND: 'BND',\n BOB: 'BOB',\n BRL: 'BRL',\n BSD: 'BSD',\n BTN: 'BTN',\n BWP: 'BWP',\n BYN: 'BYN',\n BZD: 'BZD',\n GMD: 'GMD',\n KZT: 'KZT',\n LAK: 'LAK',\n LBP: 'LBP',\n LKR: 'LKR',\n LRD: 'LRD',\n LSL: 'LSL',\n LYD: 'LYD',\n MAD: 'MAD',\n MDL: 'MDL',\n MGA: 'MGA',\n MKD: 'MKD',\n MMK: 'MMK',\n MNT: 'MNT',\n MOP: 'MOP',\n MRU: 'MRU',\n MUR: 'MUR',\n MVR: 'MVR',\n MWK: 'MWK',\n MXN: 'MXN',\n MYR: 'MYR',\n MZN: 'MZN',\n NAD: 'NAD',\n NGN: 'NGN',\n GNF: 'GNF',\n GTQ: 'GTQ',\n GYD: 'GYD',\n HKD: 'HKD',\n HNL: 'HNL',\n HTG: 'HTG',\n HUF: 'HUF',\n IDR: 'IDR',\n INR: 'INR',\n IQD: 'IQD',\n IRR: 'IRR',\n ISK: 'ISK',\n JMD: 'JMD',\n JOD: 'JOD',\n KES: 'KES',\n KGS: 'KGS',\n KHR: 'KHR',\n KMF: 'KMF',\n KPW: 'KPW',\n KRW: 'KRW',\n KWD: 'KWD',\n KYD: 'KYD',\n});\n\n/**\n * @description ISO-4217 three-letter currency code.\n */\nexport type CurrencyType = (typeof Currency)[keyof typeof Currency];\n","import { z } from \"zod\";\nimport { Currency, type CurrencyType } from \"@types\";\n\nexport const currencySchema = z\n .enum(Currency)\n .transform(val => val.toUpperCase() as CurrencyType);\n","import { z } from \"zod\";\nimport type { AllKeys, PaginationMeta } from \"@types\";\n\nexport const paginationMetaSchema = z.object({\n current_page: z.number(),\n next_page: z.number().nullable(),\n prev_page: z.number().nullable(),\n total_pages: z.number(),\n total_count: z.number(),\n} satisfies AllKeys<PaginationMeta>);\n","import { z } from \"zod\";\nimport { type CurrencyType } from \"@types\";\nimport { PaymentSource, CardScheme, CardType, PaymentStatus } from \"../enums\";\nimport {\n type CapturePaymentRequest,\n type CreateGooglePayPaymentSource,\n type CreateApplePayPaymentSource,\n type CreateSamsungPayPaymentSource,\n type CreatePaymentRequest,\n type CreateStcPayPaymentSource,\n type CreateTokenPaymentSource,\n type UpdatePaymentRequest,\n type RefundPaymentRequest,\n type WalletPaymentSource,\n type StcPaySource,\n type Payment,\n type CreateCreditCardPaymentSource,\n type BasePaymentSource,\n type CreditCardSource,\n type ListPaymentsResponse,\n} from \"../types\";\nimport type { AllKeys } from \"@types\";\n\nimport {\n amountSchema,\n currencySchema,\n paginationMetaSchema,\n} from \"@validation\";\nimport { PaymentValidation } from \"../constants\";\n\nconst BasePaymentSourceSchema = z.object({\n type: z.enum(PaymentSource),\n company: z.enum(CardScheme).nullable(),\n name: z.string().nullable(),\n number: z.string(),\n gateway_id: z.string(),\n message: z.string().nullable(),\n reference_number: z.string().nullable(),\n token: z.string().nullable().optional(),\n response_code: z.string().optional(),\n authorization_code: z\n .string()\n .regex(PaymentValidation.AUTH_CODE_REGEX)\n .optional(),\n issuer_name: z.string().optional(),\n issuer_country: z.string().optional(),\n issuer_card_type: z.enum(CardType).optional(),\n issuer_card_category: z.string().optional(),\n} satisfies AllKeys<BasePaymentSource>);\n\n// Zod schemas for validation and parsing\nconst CreditCardSourceResponseSchema = z.object({\n ...BasePaymentSourceSchema.shape,\n type: z.literal(PaymentSource.CREDITCARD),\n transaction_url: z.url().nullable(),\n} satisfies AllKeys<CreditCardSource>);\n\nconst WalletPaymentSourceResponseSchema = z.object({\n ...BasePaymentSourceSchema.shape,\n type: z.enum([\n PaymentSource.APPLEPAY,\n PaymentSource.GOOGLEPAY,\n PaymentSource.SAMSUNGPAY,\n ]),\n dpan: z.string().optional(),\n} satisfies AllKeys<WalletPaymentSource>);\n\nconst StcPaySourceResponseSchema = z.object({\n type: z.literal(PaymentSource.STCPAY),\n mobile: z.string().regex(PaymentValidation.SAUDI_MOBILE_REGEX),\n reference_number: z.string().optional(),\n cashier_id: z.string().optional(),\n branch: z.string().optional(),\n transaction_url: z.url().nullable(),\n message: z.string(),\n} satisfies AllKeys<StcPaySource>);\n\nconst PaymentSourceResponseSchema = z.discriminatedUnion(\"type\", [\n CreditCardSourceResponseSchema,\n WalletPaymentSourceResponseSchema,\n StcPaySourceResponseSchema,\n]);\n// z.discriminatedUnion(\"type\", [\n// CreditCardSourceResponseSchema,\n// WalletPaymentSourceResponseSchema,\n// StcPaySourceResponseSchema,\n// ]);\n\nexport const PaymentSchema = z.object({\n id: z.string(),\n status: z.enum(PaymentStatus),\n amount: amountSchema,\n fee: z.number().int().min(0),\n currency: currencySchema,\n refunded: z.number().int().min(0),\n refunded_at: z.coerce.date().nullable(),\n captured: z.number().int().min(0),\n captured_at: z.coerce.date().nullable(),\n voided_at: z.coerce.date().nullable(),\n description: z.string(),\n amount_format: z\n .string()\n .transform(val => val as `${number} ${CurrencyType}`),\n fee_format: z.string().transform(val => val as `${number} ${CurrencyType}`),\n refunded_format: z\n .string()\n .transform(val => val as `${number} ${CurrencyType}`),\n captured_format: z\n .string()\n .transform(val => val as `${number} ${CurrencyType}`),\n invoice_id: z.string().nullable(),\n ip: z.ipv4().nullable(),\n callback_url: z.url().nullable(),\n created_at: z.coerce.date(),\n updated_at: z.coerce.date(),\n metadata: z.record(z.string(), z.string()).nullable(),\n source: PaymentSourceResponseSchema,\n} satisfies AllKeys<Payment>);\n\nconst CreditCardSourceSchema = z.object({\n type: z.literal(PaymentSource.CREDITCARD),\n name: z\n .string()\n .min(1, \"Name is required\")\n .max(PaymentValidation.DESCRIPTION_MAX_LENGTH)\n .refine(val => val.trim().split(\" \").length >= 2, {\n message: \"Card holder name must be at least two words\",\n })\n .transform(val => val.trim()),\n number: z.string().regex(/^\\d{16,19}$/, \"Card number must be 16-19 digits\"),\n month: z\n .number()\n .int(\"Month must be an integer\")\n .min(1, \"Month must be between 1 and 12\")\n .max(12, \"Month must be between 1 and 12\"),\n year: z\n .number()\n .int(\"Year must be an integer\")\n .min(new Date().getFullYear(), \"Year cannot be in the past\"),\n cvc: z.string().regex(/^\\d{3,4}$/, \"CVC must be 3-4 digits\"),\n statement_descriptor: z\n .string()\n .max(PaymentValidation.STATEMENT_DESCRIPTOR_MAX_LENGTH)\n .optional(),\n \"3ds\": z.boolean().optional(),\n manual: z.boolean().optional(),\n save_card: z.boolean().optional(),\n} satisfies AllKeys<CreateCreditCardPaymentSource>);\n\nconst TokenSourceSchema = z.object({\n type: z.literal(PaymentSource.TOKEN),\n token: z.string().regex(/^token_/, \"Token must start with 'token_'\"),\n cvc: z\n .string()\n .regex(/^\\d{3,4}$/, \"CVC must be 3-4 digits\")\n .optional(),\n statement_descriptor: z\n .string()\n .max(PaymentValidation.STATEMENT_DESCRIPTOR_MAX_LENGTH)\n .optional(),\n \"3ds\": z.boolean().optional(),\n manual: z.boolean().optional(),\n} satisfies AllKeys<CreateTokenPaymentSource>);\n\nconst GooglePaySourceSchema = z.object({\n type: z.literal(PaymentSource.GOOGLEPAY),\n token: z.string().optional(),\n manual: z.boolean().optional(),\n save_card: z.boolean().optional(),\n statement_descriptor: z\n .string()\n .max(PaymentValidation.STATEMENT_DESCRIPTOR_MAX_LENGTH)\n .optional(),\n} satisfies AllKeys<CreateGooglePayPaymentSource>);\n\nconst ApplePaySourceSchema = z.object({\n type: z.literal(PaymentSource.APPLEPAY),\n token: z.string(),\n manual: z.boolean().optional(),\n save_card: z.boolean().optional(),\n statement_descriptor: z\n .string()\n .max(PaymentValidation.STATEMENT_DESCRIPTOR_MAX_LENGTH)\n .optional(),\n} satisfies AllKeys<CreateApplePayPaymentSource>);\n\nconst SamsungPaySourceSchema = z.object({\n type: z.literal(PaymentSource.SAMSUNGPAY),\n token: z.string(),\n manual: z.boolean().optional(),\n save_card: z.boolean().optional(),\n statement_descriptor: z\n .string()\n .max(PaymentValidation.STATEMENT_DESCRIPTOR_MAX_LENGTH)\n .optional(),\n} satisfies AllKeys<CreateSamsungPayPaymentSource>);\n\nconst StcPaySourceSchema = z.object({\n type: z.literal(PaymentSource.STCPAY),\n mobile: z\n .string()\n .regex(\n PaymentValidation.SAUDI_MOBILE_REGEX,\n \"Invalid Saudi mobile number format\"\n ),\n cashier_id: z.string().optional(),\n branch: z.string().optional(),\n} satisfies AllKeys<CreateStcPayPaymentSource>);\n\nconst CreatePaymentSourceSchema = z.discriminatedUnion(\"type\", [\n CreditCardSourceSchema,\n TokenSourceSchema,\n GooglePaySourceSchema,\n ApplePaySourceSchema,\n SamsungPaySourceSchema,\n StcPaySourceSchema,\n]);\n\nexport const CreatePaymentSchema = z.object({\n given_id: z.uuid(\"ID must be a valid UUID\").optional(),\n amount: amountSchema,\n currency: currencySchema,\n description: z\n .string()\n .min(1, \"Description is required\")\n .max(\n PaymentValidation.DESCRIPTION_MAX_LENGTH,\n `Description must be less than ${PaymentValidation.DESCRIPTION_MAX_LENGTH} characters`\n )\n .transform(val => val.trim()),\n callback_url: z.url(\"Callback URL must be a valid URL\"),\n source: CreatePaymentSourceSchema,\n metadata: z.record(z.string(), z.string()).optional(),\n apply_coupon: z.boolean().optional(),\n} satisfies AllKeys<CreatePaymentRequest>);\n\nexport const UpdatePaymentSchema = z.object({\n description: z\n .string()\n .min(1, \"Description is required\")\n .max(PaymentValidation.DESCRIPTION_MAX_LENGTH)\n .transform(val => val.trim())\n .optional(),\n metadata: z.record(z.string(), z.string()).optional(),\n} satisfies AllKeys<UpdatePaymentRequest>);\n\nexport const RefundPaymentSchema = z.object({\n amount: amountSchema.optional(),\n} satisfies AllKeys<RefundPaymentRequest>);\n\nexport const CapturePaymentSchema = z\n .object({\n amount: amountSchema.optional(),\n } satisfies AllKeys<CapturePaymentRequest>)\n .optional();\n\nexport const listPaymentResponseSchema = z.object({\n payments: z.array(z.unknown()),\n meta: paginationMetaSchema,\n} satisfies AllKeys<ListPaymentsResponse>);\n","import type { ListPaymentsResponse, Payment } from \"./types\";\nimport type {\n CreatePaymentRequest,\n UpdatePaymentRequest,\n RefundPaymentRequest,\n CapturePaymentRequest,\n} from \"./types\";\nimport { PaymentValidation } from \"./constants\";\nimport { PaymentStatus, PaymentSource, CardScheme } from \"./enums\";\nimport { type Amount, type CurrencyType } from \"@types\";\nimport {\n CreatePaymentSchema,\n UpdatePaymentSchema,\n RefundPaymentSchema,\n CapturePaymentSchema,\n PaymentSchema,\n listPaymentResponseSchema,\n} from \"./validation/schemas\";\nimport type { MetadataValidator, ValidationResult } from \"@types\";\n\ntype PaymentUtilsParams<T extends object> = {\n metadataValidator: MetadataValidator<T>;\n};\nexport class PaymentUtils<T extends object> {\n private readonly metadataValidator: MetadataValidator<T>;\n\n constructor(p: PaymentUtilsParams<T>) {\n this.metadataValidator = p.metadataValidator;\n }\n\n /**\n * Validate payment creation request using Zod\n */\n validateCreatePaymentRequest(\n request: CreatePaymentRequest<T>\n ): ValidationResult<CreatePaymentRequest<T>> {\n const result = CreatePaymentSchema.safeParse(request);\n const metadata = result.data?.metadata\n ? this.metadataValidator.parse(result.data?.metadata)\n : undefined;\n\n if (result.success) {\n return {\n success: true,\n data: {\n ...result.data,\n metadata,\n },\n errors: [],\n };\n }\n\n const errors = result.error.issues.map(err => {\n const path = err.path.length > 0 ? `${err.path.join(\".\")}: ` : \"\";\n return `${path}${err.message}`;\n });\n\n return {\n success: false,\n errors,\n };\n }\n\n /**\n * Validate payment update request using Zod\n */\n validateUpdatePaymentRequest(\n request: UpdatePaymentRequest<T>\n ): ValidationResult<UpdatePaymentRequest<T>> {\n const result = UpdatePaymentSchema.safeParse(request);\n const metadata = result.data?.metadata\n ? this.metadataValidator.parse(result.data?.metadata)\n : undefined;\n\n if (result.success) {\n return {\n success: true,\n data: {\n ...result.data,\n metadata,\n },\n errors: [],\n };\n }\n\n const errors = result.error.issues.map(err => {\n const path = err.path.length > 0 ? `${err.path.join(\".\")}: ` : \"\";\n return `${path}${err.message}`;\n });\n\n return {\n success: false,\n errors,\n };\n }\n\n /**\n * Validate refund request using Zod\n */\n validateRefundRequest(\n request: RefundPaymentRequest\n ): ValidationResult<RefundPaymentRequest> {\n const result = RefundPaymentSchema.safeParse(request);\n\n if (result.success) {\n return {\n success: true,\n data: result.data as RefundPaymentRequest,\n errors: [],\n };\n }\n\n const errors = result.error.issues.map(err => {\n const path = err.path.length > 0 ? `${err.path.join(\".\")}: ` : \"\";\n return `${path}${err.message}`;\n });\n\n return {\n success: false,\n errors,\n };\n }\n\n /**\n * Validate capture request using Zod\n */\n validateCaptureRequest(\n request?: CapturePaymentRequest\n ): ValidationResult<CapturePaymentRequest> {\n const result = CapturePaymentSchema.safeParse(request);\n\n if (result.success) {\n return {\n success: true,\n data: result.data as CapturePaymentRequest,\n errors: [],\n };\n }\n\n const errors = result.error.issues.map(err => {\n const path = err.path.length > 0 ? `${err.path.join(\".\")}: ` : \"\";\n return `${path}${err.message}`;\n });\n\n return {\n success: false,\n errors,\n };\n }\n\n /**\n * Format amount for display\n */\n formatAmount(\n amount: Amount,\n currency: CurrencyType\n ): `${number} ${CurrencyType}` {\n const divisors: Record<string, number> = {\n KWD: 1000,\n JPY: 1,\n SAR: 100,\n USD: 100,\n EUR: 100,\n };\n\n const divisor = divisors[currency] ?? 100;\n const formattedAmount = (amount / divisor).toFixed(\n divisor === 1 ? 0 : 2\n ) as `${number}`;\n\n return `${formattedAmount} ${currency}`;\n }\n\n /**\n * Parse amount from display format to smallest unit\n */\n parseAmount(formattedAmount: string, currency: CurrencyType): number {\n const divisors: Record<string, number> = {\n KWD: 1000,\n JPY: 1,\n SAR: 100,\n USD: 100,\n EUR: 100,\n };\n\n const amount = parseFloat(formattedAmount.replace(/[^\\d.]/g, \"\"));\n const divisor = divisors[currency.toUpperCase()] || 100;\n\n return Math.round(amount * divisor);\n }\n\n /**\n * Check if payment is in a final state\n */\n isPaymentFinal(status: PaymentStatus): boolean {\n const finalStatuses: PaymentStatus[] = [\n PaymentStatus.PAID,\n PaymentStatus.FAILED,\n PaymentStatus.REFUNDED,\n PaymentStatus.CAPTURED,\n PaymentStatus.VOIDED,\n PaymentStatus.VERIFIED,\n ];\n return finalStatuses.includes(status);\n }\n\n /**\n * Check if payment can be refunded\n */\n canRefundPayment(payment: Payment<T>): boolean {\n const refundableStatuses: PaymentStatus[] = [\n PaymentStatus.PAID,\n PaymentStatus.CAPTURED,\n ];\n\n // Check if status allows refund and if there's refundable amount\n const statusAllowed = refundableStatuses.includes(payment.status);\n const hasRefundableAmount = payment.captured - payment.refunded > 0;\n\n // Check if within refund timeout (if refunded_at exists, can't refund again)\n const notFullyRefunded = payment.refunded < payment.captured;\n\n return statusAllowed && hasRefundableAmount && notFullyRefunded;\n }\n\n /**\n * Check if payment can be captured\n */\n canCapturePayment(payment: Payment<T>): boolean {\n return payment.status === PaymentStatus.AUTHORIZED;\n }\n\n /**\n * Check if payment can be voided\n */\n canVoidPayment(payment: Payment<T>): boolean {\n return payment.status === PaymentStatus.AUTHORIZED;\n }\n\n /**\n * Get maximum refund amount for a payment\n */\n getMaxRefundAmount(payment: Payment<T>): number {\n return Math.max(0, payment.captured - payment.refunded);\n }\n\n /**\n * Get maximum capture amount for an authorized payment\n */\n getMaxCaptureAmount(payment: Payment<T>): number {\n if (payment.status !== PaymentStatus.AUTHORIZED) {\n return 0;\n }\n return payment.amount;\n }\n\n /**\n * Check if card scheme matches expected CVV length\n */\n validateCvcLength(cvc: string, scheme?: CardScheme): boolean {\n if (scheme === CardScheme.AMEX) {\n return cvc.length === PaymentValidation.AMEX_CVV_LENGTH;\n }\n return cvc.length === PaymentValidation.CVV_LENGTH;\n }\n\n /**\n * Mask card number for display (show first 6 and last 4 digits)\n */\n maskCardNumber(cardNumber: string): string {\n if (cardNumber.length < 10) {\n return cardNumber;\n }\n const first6 = cardNumber.substring(0, 6);\n const last4 = cardNumber.substring(cardNumber.length - 4);\n const middle = \"*\".repeat(cardNumber.length - 10);\n return `${first6}${middle}${last4}`;\n }\n\n /**\n * Get last 4 digits of card number\n */\n getCardLast4(cardNumber: string): string {\n return cardNumber.substring(cardNumber.length - 4);\n }\n\n /**\n * Build metadata query parameters for filtering\n */\n buildMetadataQuery(metadata: Record<string, string>): Record<string, string> {\n const query: Record<string, string> = {};\n\n Object.entries(metadata).forEach(([key, value]) => {\n query[`metadata[${key}]`] = value;\n });\n\n return query;\n }\n\n /**\n * Sanitize payment description\n */\n sanitizeDescription(description: string): string {\n return description\n .trim()\n .substring(0, PaymentValidation.DESCRIPTION_MAX_LENGTH);\n }\n\n /**\n * Generate idempotency key for payment\n */\n generateIdempotencyKey(prefix: string = \"pay\"): string {\n const timestamp = Date.now().toString(36);\n const random = Math.random().toString(36).substring(2, 8);\n return `${prefix}_${timestamp}_${random}`;\n }\n\n /**\n * Check if payment method requires 3DS by default\n */\n requires3DS(source: CreatePaymentRequest[\"source\"]): boolean {\n // Credit cards typically require 3DS unless explicitly bypassed\n if (source.type === PaymentSource.CREDITCARD)\n return source[\"3ds\"] !== false; // Undefined maps to true by default\n\n // Wallet payments usually handle their own authentication\n return false;\n }\n\n /**\n * Parse and validate a Payment response, ensuring all data types are correct\n */\n parsePayment(payment: unknown): Payment<T> {\n const parsed = PaymentSchema.parse(payment);\n const metadata = parsed.metadata\n ? this.metadataValidator.parse(parsed.metadata)\n : undefined;\n\n return {\n ...parsed,\n metadata,\n };\n }\n\n parseListPaymentsResponse(response: unknown): ListPaymentsResponse<T> {\n const parsed = listPaymentResponseSchema.parse(response);\n const payments = parsed.payments.map(payment => this.parsePayment(payment));\n return {\n ...parsed,\n payments,\n };\n }\n\n /**\n * Parse and validate an array of Payment responses\n */\n parsePayments(payments: unknown): Payment<T>[] {\n if (!Array.isArray(payments)) {\n throw new Error(\"Expected payments to be an array\");\n }\n return payments.map(payment => this.parsePayment(payment));\n }\n\n /**\n * Safely parse a Payment response with error handling\n */\n safeParsePayment(payment: unknown): {\n success: boolean;\n data?: Payment<T>;\n error?: string;\n } {\n try {\n const parsedPayment = this.parsePayment(payment);\n return {\n success: true,\n data: parsedPayment,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown parsing error\",\n };\n }\n }\n}\n\nexport * as PaymentSchemas from \"./validation/schemas\";\n","import type { ApiClient, Metadata, MoyasarClientTypes } from \"@types\";\nimport { API_ENDPOINTS } from \"@constants\";\nimport type {\n ListPaymentsResponse,\n PaymentListOptions,\n Payment,\n CreatePaymentRequest,\n UpdatePaymentRequest,\n RefundPaymentRequest,\n CapturePaymentRequest,\n} from \"./types\";\nimport { PaymentStatus } from \"./enums\";\nimport { PaymentUtils } from \"./utils\";\nimport { PaymentError } from \"./errors\";\nimport { MoyasarError } from \"../../shared/errors\";\n\ntype PaymentServiceParams<T extends MoyasarClientTypes> = {\n apiClient: ApiClient<T>;\n};\n\nexport class PaymentService<T extends MoyasarClientTypes> {\n private apiClient: ApiClient<T>;\n private readonly paymentUtils: PaymentUtils<T[\"metadata\"]>;\n\n constructor(p: PaymentServiceParams<T>) {\n this.apiClient = p.apiClient;\n this.paymentUtils = new PaymentUtils({\n metadataValidator: p.apiClient.metadataValidator,\n });\n }\n\n /**\n * Create a new payment\n */\n async create(\n params: CreatePaymentRequest<T[\"metadata\"]>\n ): Promise<Payment<T[\"metadata\"]>> {\n // Validate input\n const validation = this.paymentUtils.validateCreatePaymentRequest(params);\n if (!validation.success) {\n throw new PaymentError(\n `Validation failed: ${validation.errors.join(\", \")}`,\n 400\n );\n }\n\n try {\n const response = await this.apiClient.request<unknown>({\n method: \"POST\",\n url: API_ENDPOINTS.payments,\n data: params,\n });\n\n return this.paymentUtils.parsePayment(response);\n } catch (error) {\n const paymentError = this.handleError(error, `Failed to create payment`);\n throw paymentError;\n }\n }\n\n /**\n * List payments with optional filtering\n */\n async list(\n options: PaymentListOptions = {}\n ): Promise<ListPaymentsResponse<T[\"metadata\"]>> {\n try {\n // Convert metadata filters to proper query format\n const queryParams = this.parseBody(options);\n const response = await this.apiClient.request<unknown>({\n method: \"GET\",\n url: API_ENDPOINTS.payments,\n params: queryParams,\n });\n\n const parsed = this.paymentUtils.parseListPaymentsResponse(response);\n return parsed;\n } catch (error) {\n const paymentError = this.handleError(error, \"Failed to list payments\");\n throw paymentError;\n }\n }\n\n /**\n * Retrieve a specific payment\n */\n async retrieve(paymentId: string): Promise<Payment<T[\"metadata\"]>> {\n if (!paymentId) throw new PaymentError(\"Payment ID is required\", 400);\n\n try {\n const response = await this.apiClient.request<unknown>({\n method: \"GET\",\n url: `${API_ENDPOINTS.payments}/${paymentId}`,\n });\n\n // Parse and validate the response\n const payment = this.paymentUtils.parsePayment(response);\n return payment;\n } catch (error) {\n const paymentError = this.handleError(\n error,\n `Failed to retrieve payment ${paymentId}`\n );\n throw paymentError;\n }\n }\n\n /**\n * Update a payment\n */\n async update({\n paymentId,\n update,\n }: {\n paymentId: string;\n update: UpdatePaymentRequest<T[\"metadata\"]>;\n }): Promise<Payment<T[\"metadata\"]>> {\n if (!paymentId) {\n throw new PaymentError(\"Payment ID is required\", 400);\n }\n\n // Validate input\n const validation = this.paymentUtils.validateUpdatePaymentRequest(update);\n if (!validation.success) {\n throw new PaymentError(\n `Validation failed: ${validation.errors.join(\", \")}`,\n 400\n );\n }\n\n try {\n const response = await this.apiClient.request<Payment<T[\"metadata\"]>>({\n method: \"PUT\",\n url: `${API_ENDPOINTS.payments}/${paymentId}`,\n data: update,\n });\n\n return response;\n } catch (error) {\n const paymentError = this.handleError(\n error,\n `Failed to update payment ${paymentId}`\n );\n throw paymentError;\n }\n }\n\n /**\n * Refund a payment (full or partial)\n */\n async refund({\n paymentId,\n refund,\n }: {\n paymentId: string;\n refund: RefundPaymentRequest;\n }): Promise<Payment<T[\