@pandait.tech/payment-deuna 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @pandait.tech/payment-deuna
2
+
3
+ > **EXPERIMENTAL.** Este adaptador nunca procesó un pago real ni se probó contra un sandbox del
4
+ > proveedor. No lo uses en producción. Estado y plan: `docs/ROADMAP.md` del repo panda-commerce-kit.
5
+
6
+ > **VERIFICADO (2026-09-24):** `https://api.deuna.io` es el host de **DEUNA** (deuna.com), un
7
+ > orquestador de pagos regional — no de Deuna Ecuador / Banco Pichincha (deuna.ec). Cuatro hechos:
8
+ > 1. `api.deuna.io` (prod) / `api.sandbox.deuna.io` (sandbox) son de DEUNA, no de Deuna Ecuador.
9
+ > 2. DEUNA autentica con la cabecera `X-API-Key`; este cliente mandaba `X-Client-Id` +
10
+ > `X-Client-Secret`, que no coincide con ningún contrato conocido.
11
+ > 3. Las rutas `/v1/payments/prepare` y `/v1/payments/status` no existen en la documentación de
12
+ > DEUNA (su endpoint de pago es `/purchase`).
13
+ > 4. Deuna Ecuador sí ofrece QR dinámico, link de cobro y código único — la forma que este
14
+ > adaptador modela es correcta — pero su documentación técnica **no es pública**: se entrega al
15
+ > comercio recién al afiliarse. Por eso `baseUrl` ya no tiene default: es obligatorio y lo
16
+ > entrega Deuna Ecuador al dar de alta el comercio.
17
+ >
18
+ > Fuentes: `docs.deuna.com/reference/deuna-api` (DEUNA) y el artículo de soporte de Deuna Ecuador
19
+ > (`soporte.deunaapp.com`, ver `docs/ROADMAP.md` §10 del repo panda-commerce-kit).
20
+
21
+ DeUna (Banco Pichincha) wallet QR payment adapter for panda-commerce-kit. Implements `PaymentMethod`
22
+ via QR dinámico, link de pago y código único. **El transporte HTTP (rutas, cabeceras, host) no está
23
+ verificado contra documentación oficial de Deuna Ecuador** — solo la forma del cobro lo está.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pnpm add @pandait.tech/payment-deuna
29
+ ```
package/dist/index.cjs CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  // src/client.ts
4
4
  function createDeUnaClient(config) {
5
- const base = config.baseUrl ?? "https://api.deuna.io/v1";
5
+ if (!/^https?:\/\//.test(config.baseUrl ?? "")) {
6
+ throw new Error(
7
+ "DeUnaConfig.baseUrl es obligatorio: el host de la API lo entrega Deuna Ecuador (deuna.ec) al afiliar el comercio, no hay default seguro (ver README de payment-deuna)."
8
+ );
9
+ }
10
+ const base = config.baseUrl;
6
11
  const headers = {
7
12
  "X-Client-Id": config.clientId,
8
13
  "X-Client-Secret": config.clientSecret,
@@ -50,7 +55,6 @@ function toConfirmResult(s) {
50
55
  return { status: "pending" };
51
56
  }
52
57
  function toDeUnaPaymentMethod(client, opts) {
53
- const processed = opts?.processedRefs ?? /* @__PURE__ */ new Set();
54
58
  return {
55
59
  id: "deuna",
56
60
  label: "DeUna (wallet QR \xB7 Banco Pichincha)",
@@ -72,14 +76,17 @@ function toDeUnaPaymentMethod(client, opts) {
72
76
  if (opts?.onConfirm) return opts.onConfirm(ref);
73
77
  return toConfirmResult(await client.getStatus(ref));
74
78
  },
79
+ // El estado se consulta SIEMPRE, a propósito: DeUna no firma webhooks de entrada
80
+ // (el clientSecret es un header de salida hacia su API, no verificación de entrada),
81
+ // así que el status del payload no es de fiar (cualquiera que conozca la URL podría
82
+ // enviar "pagado"). La decisión sale siempre de client.getStatus(orderId); también
83
+ // cubre el webhook repetido con estado revertido (pagado → expirado).
75
84
  async handleWebhook(payload) {
76
85
  const p = payload;
77
86
  const orderId = p?.orderRef;
78
87
  if (!orderId) return { handled: false };
79
- if (processed.has(orderId)) return { handled: true, orderId };
80
- const status = p?.status ?? (await client.getStatus(orderId)).status;
81
- if (status !== PAID_STATUS) return { handled: false };
82
- processed.add(orderId);
88
+ const status = (await client.getStatus(orderId)).status;
89
+ if (status !== PAID_STATUS) return { handled: false, orderId };
83
90
  return { handled: true, orderId };
84
91
  }
85
92
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// DeUna (Banco Pichincha) — cliente HTTP mínimo\n// Wallet QR: genera QR dinámico o link de pago y consulta estado del cobro.\n\nexport interface DeUnaConfig {\n /** Client ID del comercio (DEUNA_CLIENT_ID). */\n clientId: string;\n /** Client Secret del comercio (DEUNA_CLIENT_SECRET). */\n clientSecret: string;\n /** URL a la que DeUna notificará el resultado del pago (webhook). */\n webhookUrl: string;\n /** URL de retorno después del pago (para link/código). */\n returnUrl: string;\n /** ponytail: base URL sobreescribible para tests/staging. */\n baseUrl?: string;\n}\n\nexport interface PrepareParams {\n /** Referencia única de la orden. */\n orderRef: string;\n /** Monto total en la moneda del comercio (USD). */\n amount: number;\n /** Descripción del pedido (aparece en la pantalla del wallet). */\n description?: string;\n}\n\n/** Modos de cobro que DeUna soporta. */\nexport type DeUnaPrepareMode = \"qr\" | \"link\" | \"code\";\n\nexport interface PrepareResponse {\n /** Payload del QR dinámico (string para que la UI genere la imagen). */\n qrPayload?: string;\n /** Link de pago (alternativa al QR). */\n paymentUrl?: string;\n /** Código único de 6 dígitos. */\n paymentCode?: string;\n /** ID interno de la transacción en DeUna. */\n transactionId: string;\n}\n\nexport interface TransactionStatus {\n transactionId: string;\n orderRef: string;\n /** \"PAID\" | \"PENDING\" | \"FAILED\" | \"EXPIRED\" */\n status: string;\n authorizationCode?: string;\n message?: string;\n}\n\nexport interface DeUnaClient {\n /** Genera un QR dinámico (y opcionalmente link/código) para cobrar la orden. */\n prepare(params: PrepareParams, mode?: DeUnaPrepareMode): Promise<PrepareResponse>;\n /** Consulta el estado actual de la transacción por orderRef. */\n getStatus(orderRef: string): Promise<TransactionStatus>;\n}\n\nexport function createDeUnaClient(config: DeUnaConfig): DeUnaClient {\n const base = config.baseUrl ?? \"https://api.deuna.io/v1\";\n const headers = {\n \"X-Client-Id\": config.clientId,\n \"X-Client-Secret\": config.clientSecret,\n \"Content-Type\": \"application/json\",\n };\n\n return {\n async prepare(params, mode = \"qr\") {\n const res = await fetch(`${base}/payments/prepare`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n orderRef: params.orderRef,\n amount: params.amount,\n currency: \"USD\",\n description: params.description ?? params.orderRef,\n mode,\n webhookUrl: config.webhookUrl,\n returnUrl: config.returnUrl,\n }),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna prepare ${res.status}: ${text}`);\n }\n return res.json() as Promise<PrepareResponse>;\n },\n\n async getStatus(orderRef) {\n const url = `${base}/payments/status?orderRef=${encodeURIComponent(orderRef)}`;\n const res = await fetch(url, { method: \"GET\", headers });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna getStatus ${res.status}: ${text}`);\n }\n return res.json() as Promise<TransactionStatus>;\n },\n };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita ciclo).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { DeUnaClient, TransactionStatus } from \"./client.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n}\ninterface PaymentMethod {\n id: string;\n label: string;\n prepare(order: Order): Promise<Record<string, unknown>>;\n confirm(ref: string): Promise<ConfirmResult>;\n handleWebhook(payload: unknown): Promise<WebhookResult>;\n}\n\nconst PAID_STATUS = \"PAID\";\n\nfunction toConfirmResult(s: TransactionStatus): ConfirmResult {\n if (s.status === PAID_STATUS) return { status: \"paid\", transactionId: s.transactionId };\n if (s.status === \"FAILED\" || s.status === \"EXPIRED\")\n return { status: \"failed\", error: s.message ?? s.status };\n return { status: \"pending\" };\n}\n\nexport interface DeUnaWebhookPayload {\n orderRef?: string;\n transactionId?: string;\n status?: string;\n authorizationCode?: string;\n}\n\nexport interface DeUnaPaymentMethodOpts {\n /**\n * Set inyectable para idempotencia de webhooks.\n * ponytail: en memoria por defecto; reemplaza con persistencia en DB para multi-instancia.\n */\n processedRefs?: Set<string>;\n /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */\n onConfirm?: (ref: string) => Promise<ConfirmResult>;\n}\n\nexport function toDeUnaPaymentMethod(\n client: DeUnaClient,\n opts?: DeUnaPaymentMethodOpts,\n): PaymentMethod {\n // ponytail: in-memory; reemplazar con Set persistido en DB en prod multi-instancia\n const processed = opts?.processedRefs ?? new Set<string>();\n\n return {\n id: \"deuna\",\n label: \"DeUna (wallet QR · Banco Pichincha)\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const result = await client.prepare({\n orderRef: order.id,\n amount: order.total,\n description: `Orden ${order.id}`,\n });\n // Devuelve el payload/string del QR — la UI genera la imagen (ponytail: no generar aquí).\n return {\n qrPayload: result.qrPayload,\n paymentUrl: result.paymentUrl,\n paymentCode: result.paymentCode,\n transactionId: result.transactionId,\n orderId: order.id,\n };\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n if (opts?.onConfirm) return opts.onConfirm(ref);\n return toConfirmResult(await client.getStatus(ref));\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as DeUnaWebhookPayload | null;\n const orderId = p?.orderRef;\n if (!orderId) return { handled: false };\n\n // Idempotencia: misma referencia no se procesa dos veces\n if (processed.has(orderId)) return { handled: true, orderId };\n\n // Si el webhook trae el status directamente lo usamos; si no, consultamos\n const status = p?.status ?? (await client.getStatus(orderId)).status;\n\n if (status !== PAID_STATUS) return { handled: false };\n\n processed.add(orderId);\n return { handled: true, orderId };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// DeUna (Banco Pichincha) — cliente HTTP mínimo\n// Wallet QR: genera QR dinámico o link de pago y consulta estado del cobro.\n// VERIFICADO (2026-09-24): las rutas y cabeceras de este cliente NO están confirmadas contra\n// documentación oficial de Deuna Ecuador (deuna.ec) — esa documentación no es pública, se entrega\n// al comercio recién al afiliarse. Por eso no hay baseUrl por defecto: ver README del paquete.\n\nexport interface DeUnaConfig {\n /** Client ID del comercio (DEUNA_CLIENT_ID). */\n clientId: string;\n /** Client Secret del comercio (DEUNA_CLIENT_SECRET). */\n clientSecret: string;\n /** URL a la que DeUna notificará el resultado del pago (webhook). */\n webhookUrl: string;\n /** URL de retorno después del pago (para link/código). */\n returnUrl: string;\n /** Host de la API que entrega Deuna Ecuador al afiliar el comercio. Sin default: ver README. */\n baseUrl: string;\n}\n\nexport interface PrepareParams {\n /** Referencia única de la orden. */\n orderRef: string;\n /** Monto total en la moneda del comercio (USD). */\n amount: number;\n /** Descripción del pedido (aparece en la pantalla del wallet). */\n description?: string;\n}\n\n/** Modos de cobro que DeUna soporta. */\nexport type DeUnaPrepareMode = \"qr\" | \"link\" | \"code\";\n\nexport interface PrepareResponse {\n /** Payload del QR dinámico (string para que la UI genere la imagen). */\n qrPayload?: string;\n /** Link de pago (alternativa al QR). */\n paymentUrl?: string;\n /** Código único de 6 dígitos. */\n paymentCode?: string;\n /** ID interno de la transacción en DeUna. */\n transactionId: string;\n}\n\nexport interface TransactionStatus {\n transactionId: string;\n orderRef: string;\n /** \"PAID\" | \"PENDING\" | \"FAILED\" | \"EXPIRED\" */\n status: string;\n authorizationCode?: string;\n message?: string;\n}\n\nexport interface DeUnaClient {\n /** Genera un QR dinámico (y opcionalmente link/código) para cobrar la orden. */\n prepare(params: PrepareParams, mode?: DeUnaPrepareMode): Promise<PrepareResponse>;\n /** Consulta el estado actual de la transacción por orderRef. */\n getStatus(orderRef: string): Promise<TransactionStatus>;\n}\n\nexport function createDeUnaClient(config: DeUnaConfig): DeUnaClient {\n if (!/^https?:\\/\\//.test(config.baseUrl ?? \"\")) {\n throw new Error(\n \"DeUnaConfig.baseUrl es obligatorio: el host de la API lo entrega Deuna Ecuador (deuna.ec) \" +\n \"al afiliar el comercio, no hay default seguro (ver README de payment-deuna).\",\n );\n }\n const base = config.baseUrl;\n const headers = {\n \"X-Client-Id\": config.clientId,\n \"X-Client-Secret\": config.clientSecret,\n \"Content-Type\": \"application/json\",\n };\n\n return {\n async prepare(params, mode = \"qr\") {\n const res = await fetch(`${base}/payments/prepare`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n orderRef: params.orderRef,\n amount: params.amount,\n currency: \"USD\",\n description: params.description ?? params.orderRef,\n mode,\n webhookUrl: config.webhookUrl,\n returnUrl: config.returnUrl,\n }),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna prepare ${res.status}: ${text}`);\n }\n return res.json() as Promise<PrepareResponse>;\n },\n\n async getStatus(orderRef) {\n const url = `${base}/payments/status?orderRef=${encodeURIComponent(orderRef)}`;\n const res = await fetch(url, { method: \"GET\", headers });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna getStatus ${res.status}: ${text}`);\n }\n return res.json() as Promise<TransactionStatus>;\n },\n };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita ciclo).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { DeUnaClient, TransactionStatus } from \"./client.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n}\ninterface PaymentMethod {\n id: string;\n label: string;\n prepare(order: Order): Promise<Record<string, unknown>>;\n confirm(ref: string): Promise<ConfirmResult>;\n handleWebhook(payload: unknown): Promise<WebhookResult>;\n}\n\nconst PAID_STATUS = \"PAID\";\n\nfunction toConfirmResult(s: TransactionStatus): ConfirmResult {\n if (s.status === PAID_STATUS) return { status: \"paid\", transactionId: s.transactionId };\n if (s.status === \"FAILED\" || s.status === \"EXPIRED\")\n return { status: \"failed\", error: s.message ?? s.status };\n return { status: \"pending\" };\n}\n\nexport interface DeUnaWebhookPayload {\n orderRef?: string;\n transactionId?: string;\n status?: string;\n authorizationCode?: string;\n}\n\nexport interface DeUnaPaymentMethodOpts {\n /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */\n onConfirm?: (ref: string) => Promise<ConfirmResult>;\n}\n\nexport function toDeUnaPaymentMethod(\n client: DeUnaClient,\n opts?: DeUnaPaymentMethodOpts,\n): PaymentMethod {\n return {\n id: \"deuna\",\n label: \"DeUna (wallet QR · Banco Pichincha)\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const result = await client.prepare({\n orderRef: order.id,\n amount: order.total,\n description: `Orden ${order.id}`,\n });\n // Devuelve el payload/string del QR — la UI genera la imagen (ponytail: no generar aquí).\n return {\n qrPayload: result.qrPayload,\n paymentUrl: result.paymentUrl,\n paymentCode: result.paymentCode,\n transactionId: result.transactionId,\n orderId: order.id,\n };\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n if (opts?.onConfirm) return opts.onConfirm(ref);\n return toConfirmResult(await client.getStatus(ref));\n },\n\n // El estado se consulta SIEMPRE, a propósito: DeUna no firma webhooks de entrada\n // (el clientSecret es un header de salida hacia su API, no verificación de entrada),\n // así que el status del payload no es de fiar (cualquiera que conozca la URL podría\n // enviar \"pagado\"). La decisión sale siempre de client.getStatus(orderId); también\n // cubre el webhook repetido con estado revertido (pagado → expirado).\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as DeUnaWebhookPayload | null;\n const orderId = p?.orderRef;\n if (!orderId) return { handled: false };\n\n const status = (await client.getStatus(orderId)).status;\n\n if (status !== PAID_STATUS) return { handled: false, orderId };\n\n return { handled: true, orderId };\n },\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,3 @@
1
- import { Order } from '@pandait.tech/commerce-core';
2
-
3
1
  interface DeUnaConfig {
4
2
  /** Client ID del comercio (DEUNA_CLIENT_ID). */
5
3
  clientId: string;
@@ -9,8 +7,8 @@ interface DeUnaConfig {
9
7
  webhookUrl: string;
10
8
  /** URL de retorno después del pago (para link/código). */
11
9
  returnUrl: string;
12
- /** ponytail: base URL sobreescribible para tests/staging. */
13
- baseUrl?: string;
10
+ /** Host de la API que entrega Deuna Ecuador al afiliar el comercio. Sin default: ver README. */
11
+ baseUrl: string;
14
12
  }
15
13
  interface PrepareParams {
16
14
  /** Referencia única de la orden. */
@@ -48,6 +46,76 @@ interface DeUnaClient {
48
46
  }
49
47
  declare function createDeUnaClient(config: DeUnaConfig): DeUnaClient;
50
48
 
49
+ interface FirestoreTimestamp {
50
+ toMillis(): number;
51
+ toDate(): Date;
52
+ seconds: number;
53
+ nanoseconds: number;
54
+ }
55
+
56
+ /** Shipping address captured at checkout and stored on the order. */
57
+ interface ShippingAddress {
58
+ fullName: string;
59
+ phone: string;
60
+ address: string;
61
+ city: string;
62
+ province: string;
63
+ postalCode: string;
64
+ country: string;
65
+ }
66
+ /** Buyer details captured for guest (unauthenticated) checkout. */
67
+ interface GuestInfo {
68
+ firstName: string;
69
+ lastName: string;
70
+ idNumber: string;
71
+ email: string;
72
+ phone: string;
73
+ }
74
+
75
+ type PromotionType = "percentage" | "fixed_amount" | "free_shipping";
76
+
77
+ type OrderStatus = "pending" | "processing"
78
+ /** Bank-transfer orders awaiting the customer's payment proof. */
79
+ | "awaiting_payment_proof" | "paid" | "shipped" | "delivered" | "cancelled" | "failed" | "refunded"
80
+ /** Intermediate Nuvei 3DS / OTP states (see @pandait.tech/payment-nuvei). */
81
+ | "3ds-pending" | "otp-pending";
82
+ interface OrderItem {
83
+ productId: string;
84
+ name: string;
85
+ price: number;
86
+ quantity: number;
87
+ /** Set when the purchased unit is a specific variant. */
88
+ variantSku?: string;
89
+ variantName?: string;
90
+ image?: string;
91
+ }
92
+ interface Order {
93
+ id: string;
94
+ userId: string;
95
+ items: OrderItem[];
96
+ subtotal: number;
97
+ vat: number;
98
+ shipping: number;
99
+ total: number;
100
+ status: OrderStatus;
101
+ /** Eligible payment method id, e.g. "nuvei" | "bank-transfer". */
102
+ paymentMethod?: string;
103
+ paymentToken?: string;
104
+ paymentTransactionId?: string;
105
+ authorizationCode?: string;
106
+ cardBrand?: string;
107
+ cardLast4?: string;
108
+ shippingAddress: ShippingAddress;
109
+ discount?: number;
110
+ couponCode?: string;
111
+ promotionId?: string;
112
+ discountType?: PromotionType;
113
+ /** Present for guest (unauthenticated) checkout. */
114
+ guestInfo?: GuestInfo;
115
+ createdAt: FirestoreTimestamp;
116
+ updatedAt: FirestoreTimestamp;
117
+ }
118
+
51
119
  interface ConfirmResult {
52
120
  status: "paid" | "pending" | "failed" | "challenge";
53
121
  transactionId?: string;
@@ -72,11 +140,6 @@ interface DeUnaWebhookPayload {
72
140
  authorizationCode?: string;
73
141
  }
74
142
  interface DeUnaPaymentMethodOpts {
75
- /**
76
- * Set inyectable para idempotencia de webhooks.
77
- * ponytail: en memoria por defecto; reemplaza con persistencia en DB para multi-instancia.
78
- */
79
- processedRefs?: Set<string>;
80
143
  /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */
81
144
  onConfirm?: (ref: string) => Promise<ConfirmResult>;
82
145
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { Order } from '@pandait.tech/commerce-core';
2
-
3
1
  interface DeUnaConfig {
4
2
  /** Client ID del comercio (DEUNA_CLIENT_ID). */
5
3
  clientId: string;
@@ -9,8 +7,8 @@ interface DeUnaConfig {
9
7
  webhookUrl: string;
10
8
  /** URL de retorno después del pago (para link/código). */
11
9
  returnUrl: string;
12
- /** ponytail: base URL sobreescribible para tests/staging. */
13
- baseUrl?: string;
10
+ /** Host de la API que entrega Deuna Ecuador al afiliar el comercio. Sin default: ver README. */
11
+ baseUrl: string;
14
12
  }
15
13
  interface PrepareParams {
16
14
  /** Referencia única de la orden. */
@@ -48,6 +46,76 @@ interface DeUnaClient {
48
46
  }
49
47
  declare function createDeUnaClient(config: DeUnaConfig): DeUnaClient;
50
48
 
49
+ interface FirestoreTimestamp {
50
+ toMillis(): number;
51
+ toDate(): Date;
52
+ seconds: number;
53
+ nanoseconds: number;
54
+ }
55
+
56
+ /** Shipping address captured at checkout and stored on the order. */
57
+ interface ShippingAddress {
58
+ fullName: string;
59
+ phone: string;
60
+ address: string;
61
+ city: string;
62
+ province: string;
63
+ postalCode: string;
64
+ country: string;
65
+ }
66
+ /** Buyer details captured for guest (unauthenticated) checkout. */
67
+ interface GuestInfo {
68
+ firstName: string;
69
+ lastName: string;
70
+ idNumber: string;
71
+ email: string;
72
+ phone: string;
73
+ }
74
+
75
+ type PromotionType = "percentage" | "fixed_amount" | "free_shipping";
76
+
77
+ type OrderStatus = "pending" | "processing"
78
+ /** Bank-transfer orders awaiting the customer's payment proof. */
79
+ | "awaiting_payment_proof" | "paid" | "shipped" | "delivered" | "cancelled" | "failed" | "refunded"
80
+ /** Intermediate Nuvei 3DS / OTP states (see @pandait.tech/payment-nuvei). */
81
+ | "3ds-pending" | "otp-pending";
82
+ interface OrderItem {
83
+ productId: string;
84
+ name: string;
85
+ price: number;
86
+ quantity: number;
87
+ /** Set when the purchased unit is a specific variant. */
88
+ variantSku?: string;
89
+ variantName?: string;
90
+ image?: string;
91
+ }
92
+ interface Order {
93
+ id: string;
94
+ userId: string;
95
+ items: OrderItem[];
96
+ subtotal: number;
97
+ vat: number;
98
+ shipping: number;
99
+ total: number;
100
+ status: OrderStatus;
101
+ /** Eligible payment method id, e.g. "nuvei" | "bank-transfer". */
102
+ paymentMethod?: string;
103
+ paymentToken?: string;
104
+ paymentTransactionId?: string;
105
+ authorizationCode?: string;
106
+ cardBrand?: string;
107
+ cardLast4?: string;
108
+ shippingAddress: ShippingAddress;
109
+ discount?: number;
110
+ couponCode?: string;
111
+ promotionId?: string;
112
+ discountType?: PromotionType;
113
+ /** Present for guest (unauthenticated) checkout. */
114
+ guestInfo?: GuestInfo;
115
+ createdAt: FirestoreTimestamp;
116
+ updatedAt: FirestoreTimestamp;
117
+ }
118
+
51
119
  interface ConfirmResult {
52
120
  status: "paid" | "pending" | "failed" | "challenge";
53
121
  transactionId?: string;
@@ -72,11 +140,6 @@ interface DeUnaWebhookPayload {
72
140
  authorizationCode?: string;
73
141
  }
74
142
  interface DeUnaPaymentMethodOpts {
75
- /**
76
- * Set inyectable para idempotencia de webhooks.
77
- * ponytail: en memoria por defecto; reemplaza con persistencia en DB para multi-instancia.
78
- */
79
- processedRefs?: Set<string>;
80
143
  /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */
81
144
  onConfirm?: (ref: string) => Promise<ConfirmResult>;
82
145
  }
package/dist/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  // src/client.ts
2
2
  function createDeUnaClient(config) {
3
- const base = config.baseUrl ?? "https://api.deuna.io/v1";
3
+ if (!/^https?:\/\//.test(config.baseUrl ?? "")) {
4
+ throw new Error(
5
+ "DeUnaConfig.baseUrl es obligatorio: el host de la API lo entrega Deuna Ecuador (deuna.ec) al afiliar el comercio, no hay default seguro (ver README de payment-deuna)."
6
+ );
7
+ }
8
+ const base = config.baseUrl;
4
9
  const headers = {
5
10
  "X-Client-Id": config.clientId,
6
11
  "X-Client-Secret": config.clientSecret,
@@ -48,7 +53,6 @@ function toConfirmResult(s) {
48
53
  return { status: "pending" };
49
54
  }
50
55
  function toDeUnaPaymentMethod(client, opts) {
51
- const processed = opts?.processedRefs ?? /* @__PURE__ */ new Set();
52
56
  return {
53
57
  id: "deuna",
54
58
  label: "DeUna (wallet QR \xB7 Banco Pichincha)",
@@ -70,14 +74,17 @@ function toDeUnaPaymentMethod(client, opts) {
70
74
  if (opts?.onConfirm) return opts.onConfirm(ref);
71
75
  return toConfirmResult(await client.getStatus(ref));
72
76
  },
77
+ // El estado se consulta SIEMPRE, a propósito: DeUna no firma webhooks de entrada
78
+ // (el clientSecret es un header de salida hacia su API, no verificación de entrada),
79
+ // así que el status del payload no es de fiar (cualquiera que conozca la URL podría
80
+ // enviar "pagado"). La decisión sale siempre de client.getStatus(orderId); también
81
+ // cubre el webhook repetido con estado revertido (pagado → expirado).
73
82
  async handleWebhook(payload) {
74
83
  const p = payload;
75
84
  const orderId = p?.orderRef;
76
85
  if (!orderId) return { handled: false };
77
- if (processed.has(orderId)) return { handled: true, orderId };
78
- const status = p?.status ?? (await client.getStatus(orderId)).status;
79
- if (status !== PAID_STATUS) return { handled: false };
80
- processed.add(orderId);
86
+ const status = (await client.getStatus(orderId)).status;
87
+ if (status !== PAID_STATUS) return { handled: false, orderId };
81
88
  return { handled: true, orderId };
82
89
  }
83
90
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// DeUna (Banco Pichincha) — cliente HTTP mínimo\n// Wallet QR: genera QR dinámico o link de pago y consulta estado del cobro.\n\nexport interface DeUnaConfig {\n /** Client ID del comercio (DEUNA_CLIENT_ID). */\n clientId: string;\n /** Client Secret del comercio (DEUNA_CLIENT_SECRET). */\n clientSecret: string;\n /** URL a la que DeUna notificará el resultado del pago (webhook). */\n webhookUrl: string;\n /** URL de retorno después del pago (para link/código). */\n returnUrl: string;\n /** ponytail: base URL sobreescribible para tests/staging. */\n baseUrl?: string;\n}\n\nexport interface PrepareParams {\n /** Referencia única de la orden. */\n orderRef: string;\n /** Monto total en la moneda del comercio (USD). */\n amount: number;\n /** Descripción del pedido (aparece en la pantalla del wallet). */\n description?: string;\n}\n\n/** Modos de cobro que DeUna soporta. */\nexport type DeUnaPrepareMode = \"qr\" | \"link\" | \"code\";\n\nexport interface PrepareResponse {\n /** Payload del QR dinámico (string para que la UI genere la imagen). */\n qrPayload?: string;\n /** Link de pago (alternativa al QR). */\n paymentUrl?: string;\n /** Código único de 6 dígitos. */\n paymentCode?: string;\n /** ID interno de la transacción en DeUna. */\n transactionId: string;\n}\n\nexport interface TransactionStatus {\n transactionId: string;\n orderRef: string;\n /** \"PAID\" | \"PENDING\" | \"FAILED\" | \"EXPIRED\" */\n status: string;\n authorizationCode?: string;\n message?: string;\n}\n\nexport interface DeUnaClient {\n /** Genera un QR dinámico (y opcionalmente link/código) para cobrar la orden. */\n prepare(params: PrepareParams, mode?: DeUnaPrepareMode): Promise<PrepareResponse>;\n /** Consulta el estado actual de la transacción por orderRef. */\n getStatus(orderRef: string): Promise<TransactionStatus>;\n}\n\nexport function createDeUnaClient(config: DeUnaConfig): DeUnaClient {\n const base = config.baseUrl ?? \"https://api.deuna.io/v1\";\n const headers = {\n \"X-Client-Id\": config.clientId,\n \"X-Client-Secret\": config.clientSecret,\n \"Content-Type\": \"application/json\",\n };\n\n return {\n async prepare(params, mode = \"qr\") {\n const res = await fetch(`${base}/payments/prepare`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n orderRef: params.orderRef,\n amount: params.amount,\n currency: \"USD\",\n description: params.description ?? params.orderRef,\n mode,\n webhookUrl: config.webhookUrl,\n returnUrl: config.returnUrl,\n }),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna prepare ${res.status}: ${text}`);\n }\n return res.json() as Promise<PrepareResponse>;\n },\n\n async getStatus(orderRef) {\n const url = `${base}/payments/status?orderRef=${encodeURIComponent(orderRef)}`;\n const res = await fetch(url, { method: \"GET\", headers });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna getStatus ${res.status}: ${text}`);\n }\n return res.json() as Promise<TransactionStatus>;\n },\n };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita ciclo).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { DeUnaClient, TransactionStatus } from \"./client.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n}\ninterface PaymentMethod {\n id: string;\n label: string;\n prepare(order: Order): Promise<Record<string, unknown>>;\n confirm(ref: string): Promise<ConfirmResult>;\n handleWebhook(payload: unknown): Promise<WebhookResult>;\n}\n\nconst PAID_STATUS = \"PAID\";\n\nfunction toConfirmResult(s: TransactionStatus): ConfirmResult {\n if (s.status === PAID_STATUS) return { status: \"paid\", transactionId: s.transactionId };\n if (s.status === \"FAILED\" || s.status === \"EXPIRED\")\n return { status: \"failed\", error: s.message ?? s.status };\n return { status: \"pending\" };\n}\n\nexport interface DeUnaWebhookPayload {\n orderRef?: string;\n transactionId?: string;\n status?: string;\n authorizationCode?: string;\n}\n\nexport interface DeUnaPaymentMethodOpts {\n /**\n * Set inyectable para idempotencia de webhooks.\n * ponytail: en memoria por defecto; reemplaza con persistencia en DB para multi-instancia.\n */\n processedRefs?: Set<string>;\n /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */\n onConfirm?: (ref: string) => Promise<ConfirmResult>;\n}\n\nexport function toDeUnaPaymentMethod(\n client: DeUnaClient,\n opts?: DeUnaPaymentMethodOpts,\n): PaymentMethod {\n // ponytail: in-memory; reemplazar con Set persistido en DB en prod multi-instancia\n const processed = opts?.processedRefs ?? new Set<string>();\n\n return {\n id: \"deuna\",\n label: \"DeUna (wallet QR · Banco Pichincha)\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const result = await client.prepare({\n orderRef: order.id,\n amount: order.total,\n description: `Orden ${order.id}`,\n });\n // Devuelve el payload/string del QR — la UI genera la imagen (ponytail: no generar aquí).\n return {\n qrPayload: result.qrPayload,\n paymentUrl: result.paymentUrl,\n paymentCode: result.paymentCode,\n transactionId: result.transactionId,\n orderId: order.id,\n };\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n if (opts?.onConfirm) return opts.onConfirm(ref);\n return toConfirmResult(await client.getStatus(ref));\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as DeUnaWebhookPayload | null;\n const orderId = p?.orderRef;\n if (!orderId) return { handled: false };\n\n // Idempotencia: misma referencia no se procesa dos veces\n if (processed.has(orderId)) return { handled: true, orderId };\n\n // Si el webhook trae el status directamente lo usamos; si no, consultamos\n const status = p?.status ?? (await client.getStatus(orderId)).status;\n\n if (status !== PAID_STATUS) return { handled: false };\n\n processed.add(orderId);\n return { handled: true, orderId };\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// DeUna (Banco Pichincha) — cliente HTTP mínimo\n// Wallet QR: genera QR dinámico o link de pago y consulta estado del cobro.\n// VERIFICADO (2026-09-24): las rutas y cabeceras de este cliente NO están confirmadas contra\n// documentación oficial de Deuna Ecuador (deuna.ec) — esa documentación no es pública, se entrega\n// al comercio recién al afiliarse. Por eso no hay baseUrl por defecto: ver README del paquete.\n\nexport interface DeUnaConfig {\n /** Client ID del comercio (DEUNA_CLIENT_ID). */\n clientId: string;\n /** Client Secret del comercio (DEUNA_CLIENT_SECRET). */\n clientSecret: string;\n /** URL a la que DeUna notificará el resultado del pago (webhook). */\n webhookUrl: string;\n /** URL de retorno después del pago (para link/código). */\n returnUrl: string;\n /** Host de la API que entrega Deuna Ecuador al afiliar el comercio. Sin default: ver README. */\n baseUrl: string;\n}\n\nexport interface PrepareParams {\n /** Referencia única de la orden. */\n orderRef: string;\n /** Monto total en la moneda del comercio (USD). */\n amount: number;\n /** Descripción del pedido (aparece en la pantalla del wallet). */\n description?: string;\n}\n\n/** Modos de cobro que DeUna soporta. */\nexport type DeUnaPrepareMode = \"qr\" | \"link\" | \"code\";\n\nexport interface PrepareResponse {\n /** Payload del QR dinámico (string para que la UI genere la imagen). */\n qrPayload?: string;\n /** Link de pago (alternativa al QR). */\n paymentUrl?: string;\n /** Código único de 6 dígitos. */\n paymentCode?: string;\n /** ID interno de la transacción en DeUna. */\n transactionId: string;\n}\n\nexport interface TransactionStatus {\n transactionId: string;\n orderRef: string;\n /** \"PAID\" | \"PENDING\" | \"FAILED\" | \"EXPIRED\" */\n status: string;\n authorizationCode?: string;\n message?: string;\n}\n\nexport interface DeUnaClient {\n /** Genera un QR dinámico (y opcionalmente link/código) para cobrar la orden. */\n prepare(params: PrepareParams, mode?: DeUnaPrepareMode): Promise<PrepareResponse>;\n /** Consulta el estado actual de la transacción por orderRef. */\n getStatus(orderRef: string): Promise<TransactionStatus>;\n}\n\nexport function createDeUnaClient(config: DeUnaConfig): DeUnaClient {\n if (!/^https?:\\/\\//.test(config.baseUrl ?? \"\")) {\n throw new Error(\n \"DeUnaConfig.baseUrl es obligatorio: el host de la API lo entrega Deuna Ecuador (deuna.ec) \" +\n \"al afiliar el comercio, no hay default seguro (ver README de payment-deuna).\",\n );\n }\n const base = config.baseUrl;\n const headers = {\n \"X-Client-Id\": config.clientId,\n \"X-Client-Secret\": config.clientSecret,\n \"Content-Type\": \"application/json\",\n };\n\n return {\n async prepare(params, mode = \"qr\") {\n const res = await fetch(`${base}/payments/prepare`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n orderRef: params.orderRef,\n amount: params.amount,\n currency: \"USD\",\n description: params.description ?? params.orderRef,\n mode,\n webhookUrl: config.webhookUrl,\n returnUrl: config.returnUrl,\n }),\n });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna prepare ${res.status}: ${text}`);\n }\n return res.json() as Promise<PrepareResponse>;\n },\n\n async getStatus(orderRef) {\n const url = `${base}/payments/status?orderRef=${encodeURIComponent(orderRef)}`;\n const res = await fetch(url, { method: \"GET\", headers });\n if (!res.ok) {\n const text = await res.text().catch(() => \"\");\n throw new Error(`DeUna getStatus ${res.status}: ${text}`);\n }\n return res.json() as Promise<TransactionStatus>;\n },\n };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita ciclo).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { DeUnaClient, TransactionStatus } from \"./client.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n}\ninterface PaymentMethod {\n id: string;\n label: string;\n prepare(order: Order): Promise<Record<string, unknown>>;\n confirm(ref: string): Promise<ConfirmResult>;\n handleWebhook(payload: unknown): Promise<WebhookResult>;\n}\n\nconst PAID_STATUS = \"PAID\";\n\nfunction toConfirmResult(s: TransactionStatus): ConfirmResult {\n if (s.status === PAID_STATUS) return { status: \"paid\", transactionId: s.transactionId };\n if (s.status === \"FAILED\" || s.status === \"EXPIRED\")\n return { status: \"failed\", error: s.message ?? s.status };\n return { status: \"pending\" };\n}\n\nexport interface DeUnaWebhookPayload {\n orderRef?: string;\n transactionId?: string;\n status?: string;\n authorizationCode?: string;\n}\n\nexport interface DeUnaPaymentMethodOpts {\n /** Override de confirm para entornos sin acceso directo a la API (tests, staging). */\n onConfirm?: (ref: string) => Promise<ConfirmResult>;\n}\n\nexport function toDeUnaPaymentMethod(\n client: DeUnaClient,\n opts?: DeUnaPaymentMethodOpts,\n): PaymentMethod {\n return {\n id: \"deuna\",\n label: \"DeUna (wallet QR · Banco Pichincha)\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const result = await client.prepare({\n orderRef: order.id,\n amount: order.total,\n description: `Orden ${order.id}`,\n });\n // Devuelve el payload/string del QR — la UI genera la imagen (ponytail: no generar aquí).\n return {\n qrPayload: result.qrPayload,\n paymentUrl: result.paymentUrl,\n paymentCode: result.paymentCode,\n transactionId: result.transactionId,\n orderId: order.id,\n };\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n if (opts?.onConfirm) return opts.onConfirm(ref);\n return toConfirmResult(await client.getStatus(ref));\n },\n\n // El estado se consulta SIEMPRE, a propósito: DeUna no firma webhooks de entrada\n // (el clientSecret es un header de salida hacia su API, no verificación de entrada),\n // así que el status del payload no es de fiar (cualquiera que conozca la URL podría\n // enviar \"pagado\"). La decisión sale siempre de client.getStatus(orderId); también\n // cubre el webhook repetido con estado revertido (pagado → expirado).\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as DeUnaWebhookPayload | null;\n const orderId = p?.orderRef;\n if (!orderId) return { handled: false };\n\n const status = (await client.getStatus(orderId)).status;\n\n if (status !== PAID_STATUS) return { handled: false, orderId };\n\n return { handled: true, orderId };\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pandait.tech/payment-deuna",
3
- "version": "1.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "DeUna (Banco Pichincha) wallet QR payment adapter for panda-commerce-kit. Implements PaymentMethod via QR dinámico, link de pago y código único.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -45,7 +45,7 @@
45
45
  "tsup": "^8.3.0",
46
46
  "typescript": "^5.6.3",
47
47
  "vitest": "^4.1.7",
48
- "@pandait.tech/commerce-core": "0.0.0"
48
+ "@pandait.tech/commerce-core": "0.1.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup",