@pandait.tech/payment-bank-transfer 1.0.0 → 2.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 +37 -9
- package/dist/index.cjs +37 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -3
- package/dist/index.d.ts +28 -3
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,16 +4,30 @@ Adapter de transferencia bancaria con comprobante para `panda-commerce-kit`. Imp
|
|
|
4
4
|
`PaymentMethod` sin red: no llama a ninguna API externa, no recibe webhooks de un banco, no sube ni
|
|
5
5
|
almacena el archivo del comprobante (solo su metadata).
|
|
6
6
|
|
|
7
|
+
## Canales
|
|
8
|
+
|
|
9
|
+
Un solo método de pago ("transferencia"), con **canales** habilitables por cliente en
|
|
10
|
+
`config.channels`. Cada canal es de dos tipos:
|
|
11
|
+
|
|
12
|
+
- **`bank`** — una cuenta bancaria (`account: BankAccount`).
|
|
13
|
+
- **`wallet-qr`** — el QR **estático** del comercio para DeUna o peiGo (`wallet`, `qrImageUrl`,
|
|
14
|
+
`holder`, `phone?`). Ninguno de los dos tiene aquí una API de comercio: el QR es fijo y no lleva
|
|
15
|
+
monto ni orden, por eso el comprobante sigue siendo obligatorio.
|
|
16
|
+
|
|
17
|
+
Un canal se apaga con `enabled: false` sin borrarlo de la configuración. Los `id` deben ser únicos:
|
|
18
|
+
`toBankTransferPaymentMethod` lanza al construirse si hay dos canales con el mismo `id`.
|
|
19
|
+
|
|
7
20
|
## Flujo
|
|
8
21
|
|
|
9
|
-
1. **`prepare(order)`** — devuelve
|
|
10
|
-
el monto y un `whatsappUrl` opcional para que
|
|
11
|
-
`awaiting_payment_proof`.
|
|
22
|
+
1. **`prepare(order)`** — devuelve los canales habilitados (`config.channels` filtrados por
|
|
23
|
+
`enabled !== false`), la referencia (`order.id`), el monto y un `whatsappUrl` opcional para que
|
|
24
|
+
el cliente avise por WhatsApp. La orden pasa a `awaiting_payment_proof`. Si no queda ningún canal
|
|
25
|
+
habilitado, lanza un error en vez de devolver un checkout sin formas de pagar.
|
|
12
26
|
2. El cliente transfiere y **la app** (fuera de este package) sube el archivo del comprobante a
|
|
13
|
-
almacenamiento y llama a **`submitProof(input)`** con su metadata (`
|
|
14
|
-
`sizeBytes`, `referenceNumber`, `declaredAmount`). `submitProof` busca la orden y
|
|
15
|
-
referencia ya se usó (vía `ports`), corre `validateProof` — puro y síncrono — y,
|
|
16
|
-
válido, invoca `ports.onProofAccepted`.
|
|
27
|
+
almacenamiento y llama a **`submitProof(input)`** con su metadata (`channelId`, `url`,
|
|
28
|
+
`mimeType`, `sizeBytes`, `referenceNumber`, `declaredAmount`). `submitProof` busca la orden y
|
|
29
|
+
valida si la referencia ya se usó (vía `ports`), corre `validateProof` — puro y síncrono — y,
|
|
30
|
+
solo si es válido, invoca `ports.onProofAccepted`.
|
|
17
31
|
3. Un admin revisa el comprobante y aprueba o rechaza. Esa decisión llega como
|
|
18
32
|
**`handleWebhook({ orderRef, decision })`** — aquí "el webhook" es la decisión del admin, no un
|
|
19
33
|
banco.
|
|
@@ -46,6 +60,7 @@ sin mocks async):
|
|
|
46
60
|
|---|---|
|
|
47
61
|
| `order-not-found` | La orden no existe |
|
|
48
62
|
| `order-not-awaiting-proof` | La orden no está en `awaiting_payment_proof` (ya `paid`, `cancelled`, …) — bloquea re-envío y replay sobre una orden ya cobrada |
|
|
63
|
+
| `unknown-channel` | `channelId` no corresponde a ningún canal habilitado — el mensaje lista los `id` habilitados |
|
|
49
64
|
| `unsupported-file` | `mimeType` fuera del allowlist, o `sizeBytes` fuera de rango |
|
|
50
65
|
| `amount-below-total` | `declaredAmount < order.total` (pagar de más se acepta) |
|
|
51
66
|
| `duplicate-reference` | El mismo número de referencia ya se usó en otra orden — el fraude real de este método |
|
|
@@ -59,8 +74,21 @@ import { toBankTransferPaymentMethod } from "@pandait.tech/payment-bank-transfer
|
|
|
59
74
|
|
|
60
75
|
const bankTransfer = toBankTransferPaymentMethod(
|
|
61
76
|
{
|
|
62
|
-
|
|
63
|
-
{
|
|
77
|
+
channels: [
|
|
78
|
+
{
|
|
79
|
+
id: "banco-pichincha",
|
|
80
|
+
kind: "bank",
|
|
81
|
+
label: "Banco Pichincha — corriente",
|
|
82
|
+
account: { bank: "Banco Pichincha", holder: "Mi Tienda SAS", accountNumber: "123", accountType: "corriente", taxId: "099..." },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "qr-deuna",
|
|
86
|
+
kind: "wallet-qr",
|
|
87
|
+
label: "QR DeUna",
|
|
88
|
+
wallet: "deuna",
|
|
89
|
+
qrImageUrl: "https://storage.mitienda.com/qr-deuna.png",
|
|
90
|
+
holder: "Mi Tienda SAS",
|
|
91
|
+
},
|
|
64
92
|
],
|
|
65
93
|
whatsapp: "593999999999",
|
|
66
94
|
},
|
package/dist/index.cjs
CHANGED
|
@@ -13,7 +13,7 @@ var DEFAULT_ALLOWED_MIME_TYPES = [
|
|
|
13
13
|
var money = (n) => `$${n.toFixed(2)}`;
|
|
14
14
|
var megabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
15
15
|
function validateProof(input, ctx) {
|
|
16
|
-
const { order, referenceAlreadyUsed, config } = ctx;
|
|
16
|
+
const { order, referenceAlreadyUsed, config, channels } = ctx;
|
|
17
17
|
if (!order) {
|
|
18
18
|
return {
|
|
19
19
|
ok: false,
|
|
@@ -28,6 +28,14 @@ function validateProof(input, ctx) {
|
|
|
28
28
|
message: `La orden ${order.id} est\xE1 en estado "${order.status}" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
if (!channels.some((channel) => channel.id === input.channelId)) {
|
|
32
|
+
const enabledIds = channels.map((channel) => channel.id).join(", ") || "ninguno";
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
reason: "unknown-channel",
|
|
36
|
+
message: `El canal de cobro "${input.channelId}" no existe o est\xE1 deshabilitado. Canales habilitados: ${enabledIds}.`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
31
39
|
const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;
|
|
32
40
|
const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;
|
|
33
41
|
if (!allowedTypes.includes(input.mimeType) || input.sizeBytes <= 0 || input.sizeBytes > maxBytes) {
|
|
@@ -55,17 +63,38 @@ function validateProof(input, ctx) {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
// src/adapter.ts
|
|
66
|
+
function enabledChannels(config) {
|
|
67
|
+
return config.channels.filter((channel) => channel.enabled !== false);
|
|
68
|
+
}
|
|
69
|
+
function assertNoDuplicateChannelIds(channels) {
|
|
70
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71
|
+
for (const channel of channels) {
|
|
72
|
+
if (seen.has(channel.id)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Configuraci\xF3n de payment-bank-transfer inv\xE1lida: el id de canal "${channel.id}" est\xE1 repetido en channels.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
seen.add(channel.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
function buildWhatsappUrl(whatsapp, orderId) {
|
|
59
81
|
const text = encodeURIComponent(`Hola, env\xEDo el comprobante de transferencia de la orden ${orderId}.`);
|
|
60
82
|
return `https://wa.me/${whatsapp}?text=${text}`;
|
|
61
83
|
}
|
|
62
84
|
function toBankTransferPaymentMethod(config, ports) {
|
|
85
|
+
assertNoDuplicateChannelIds(config.channels);
|
|
63
86
|
return {
|
|
64
87
|
id: "bank-transfer",
|
|
65
88
|
label: "Transferencia bancaria",
|
|
66
89
|
async prepare(order) {
|
|
90
|
+
const channels = enabledChannels(config);
|
|
91
|
+
if (channels.length === 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"No hay ning\xFAn canal de cobro habilitado en la configuraci\xF3n (channels): revisa que al menos uno tenga enabled !== false."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
67
96
|
return {
|
|
68
|
-
|
|
97
|
+
channels,
|
|
69
98
|
reference: order.id,
|
|
70
99
|
amount: order.total,
|
|
71
100
|
status: "awaiting_payment_proof",
|
|
@@ -77,7 +106,12 @@ function toBankTransferPaymentMethod(config, ports) {
|
|
|
77
106
|
ports.findOrder(input.orderRef),
|
|
78
107
|
ports.isReferenceUsed(input.referenceNumber)
|
|
79
108
|
]);
|
|
80
|
-
const check = validateProof(input, {
|
|
109
|
+
const check = validateProof(input, {
|
|
110
|
+
order,
|
|
111
|
+
referenceAlreadyUsed,
|
|
112
|
+
config,
|
|
113
|
+
channels: enabledChannels(config)
|
|
114
|
+
});
|
|
81
115
|
if (check.ok && order) {
|
|
82
116
|
await ports.onProofAccepted(input, order);
|
|
83
117
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/proof.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export interface BankAccount {\n bank: string;\n holder: string;\n accountNumber: string;\n accountType: \"ahorros\" | \"corriente\";\n /** Cédula/RUC del titular. */\n taxId: string;\n}\n\nexport interface BankTransferConfig {\n accounts: BankAccount[];\n /** E.164 sin '+', para el link wa.me. */\n whatsapp?: string;\n maxProofBytes?: number;\n allowedMimeTypes?: readonly string[];\n}\n\nexport const DEFAULT_MAX_PROOF_BYTES = 5 * 1024 * 1024;\nexport const DEFAULT_ALLOWED_MIME_TYPES: readonly string[] = [\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"application/pdf\",\n];\n","import type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig } from \"./config.js\";\nimport { DEFAULT_MAX_PROOF_BYTES, DEFAULT_ALLOWED_MIME_TYPES } from \"./config.js\";\n\nexport interface ProofInput {\n orderRef: string;\n url: string;\n mimeType: string;\n sizeBytes: number;\n /** Número de transacción/documento del banco. */\n referenceNumber: string;\n declaredAmount: number;\n depositedAt?: string;\n}\n\nexport type ProofRejection =\n | \"order-not-found\"\n | \"order-not-awaiting-proof\"\n | \"unsupported-file\"\n | \"amount-below-total\"\n | \"duplicate-reference\";\n\nexport type ProofCheck = { ok: true } | { ok: false; reason: ProofRejection; message: string };\n\nexport interface ProofCheckContext {\n order?: Order;\n referenceAlreadyUsed: boolean;\n config: BankTransferConfig;\n}\n\nconst money = (n: number) => `$${n.toFixed(2)}`;\nconst megabytes = (bytes: number) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\nexport function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck {\n const { order, referenceAlreadyUsed, config } = ctx;\n\n if (!order) {\n return {\n ok: false,\n reason: \"order-not-found\",\n message: `No existe una orden con referencia \"${input.orderRef}\". Verifica el código de orden.`,\n };\n }\n\n if (order.status !== \"awaiting_payment_proof\") {\n return {\n ok: false,\n reason: \"order-not-awaiting-proof\",\n message: `La orden ${order.id} está en estado \"${order.status}\" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`,\n };\n }\n\n const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;\n const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;\n if (\n !allowedTypes.includes(input.mimeType) ||\n input.sizeBytes <= 0 ||\n input.sizeBytes > maxBytes\n ) {\n return {\n ok: false,\n reason: \"unsupported-file\",\n message: `El archivo debe ser ${allowedTypes.join(\", \")} y pesar hasta ${megabytes(maxBytes)}. Sube el comprobante en un formato válido.`,\n };\n }\n\n if (input.declaredAmount < order.total) {\n return {\n ok: false,\n reason: \"amount-below-total\",\n message: `El monto declarado (${money(input.declaredAmount)}) es menor al total de la orden (${money(order.total)}). Sube un comprobante por el total.`,\n };\n }\n\n if (referenceAlreadyUsed) {\n return {\n ok: false,\n reason: \"duplicate-reference\",\n message: `El número de referencia \"${input.referenceNumber}\" ya fue usado en otra orden. Cada comprobante debe tener una referencia única.`,\n };\n }\n\n return { ok: true };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita dep de workspace en runtime).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig } from \"./config.js\";\nimport type { ProofCheck, ProofInput } from \"./proof.js\";\nimport { validateProof } from \"./proof.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\n\n/**\n * Contrato de handleWebhook (§4 del brief): \"esta orden se pagó, finalízala\" solo cuando\n * handled === true. Un rechazo NO es un fallo silencioso: viaja como\n * { handled: false, orderId, decision: \"rejected\" }, no como { handled: false } a secas.\n */\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n decision?: \"approved\" | \"rejected\";\n}\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\nexport interface BankTransferPaymentMethod extends PaymentMethod {\n /** Valida y registra el comprobante subido por el cliente para una orden. */\n submitProof(input: ProofInput): Promise<ProofCheck>;\n}\n\nexport interface BankTransferWebhookPayload {\n orderRef?: string;\n decision?: \"approved\" | \"rejected\";\n reviewerId?: string;\n reviewedAt?: string;\n}\n\nexport interface BankTransferPorts {\n findOrder(orderRef: string): Promise<Order | undefined>;\n isReferenceUsed(referenceNumber: string): Promise<boolean>;\n onProofAccepted(input: ProofInput, order: Order): Promise<void>;\n /** Decisión del admin para una orden; ausente/undefined mientras esté en revisión. */\n getDecision?(orderRef: string): Promise<\"approved\" | \"rejected\" | undefined>;\n}\n\nfunction buildWhatsappUrl(whatsapp: string, orderId: string): string {\n const text = encodeURIComponent(`Hola, envío el comprobante de transferencia de la orden ${orderId}.`);\n return `https://wa.me/${whatsapp}?text=${text}`;\n}\n\nexport function toBankTransferPaymentMethod(\n config: BankTransferConfig,\n ports: BankTransferPorts,\n): BankTransferPaymentMethod {\n return {\n id: \"bank-transfer\",\n label: \"Transferencia bancaria\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n return {\n accounts: config.accounts,\n reference: order.id,\n amount: order.total,\n status: \"awaiting_payment_proof\",\n ...(config.whatsapp ? { whatsappUrl: buildWhatsappUrl(config.whatsapp, order.id) } : {}),\n };\n },\n\n async submitProof(input: ProofInput): Promise<ProofCheck> {\n const [order, referenceAlreadyUsed] = await Promise.all([\n ports.findOrder(input.orderRef),\n ports.isReferenceUsed(input.referenceNumber),\n ]);\n\n const check = validateProof(input, { order, referenceAlreadyUsed, config });\n if (check.ok && order) {\n await ports.onProofAccepted(input, order);\n }\n return check;\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n const decision = await ports.getDecision?.(ref);\n if (decision === \"approved\") return { status: \"paid\" };\n if (decision === \"rejected\") return { status: \"failed\", error: \"Comprobante rechazado\" };\n return { status: \"pending\" };\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as BankTransferWebhookPayload | null;\n if (!p?.orderRef || (p.decision !== \"approved\" && p.decision !== \"rejected\")) {\n return { handled: false };\n }\n const { orderRef, decision } = p;\n\n if (decision === \"rejected\") {\n return { handled: false, orderId: orderRef, decision };\n }\n\n return { handled: true, orderId: orderRef, decision };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/proof.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export interface BankAccount {\n bank: string;\n holder: string;\n accountNumber: string;\n accountType: \"ahorros\" | \"corriente\";\n /** Cédula/RUC del titular. */\n taxId: string;\n}\n\ninterface ChannelBase {\n /** Único dentro de la configuración. Viaja en el comprobante. */\n id: string;\n /** Lo que ve el comprador. Ej. \"Banco Pichincha — ahorros\". */\n label: string;\n /** Default: true. Se apaga sin borrar el canal de la configuración. */\n enabled?: boolean;\n}\n\nexport type TransferChannel =\n | (ChannelBase & { kind: \"bank\"; account: BankAccount })\n | (ChannelBase & {\n kind: \"wallet-qr\";\n wallet: \"deuna\" | \"peigo\";\n // ponytail: QR estático sin monto — se cambia a QR dinámico cuando un comercio tenga contrato de API\n /** Imagen del QR ESTÁTICO del comercio. La sube el comercio; el kit no la genera. */\n qrImageUrl: string;\n /** Titular de la billetera, para que el comprador confirme a quién paga. */\n holder: string;\n /** Teléfono de la billetera, si el comercio también cobra por número. */\n phone?: string;\n });\n\nexport interface BankTransferConfig {\n channels: TransferChannel[];\n /** E.164 sin '+', para el link wa.me. */\n whatsapp?: string;\n maxProofBytes?: number;\n allowedMimeTypes?: readonly string[];\n}\n\nexport const DEFAULT_MAX_PROOF_BYTES = 5 * 1024 * 1024;\nexport const DEFAULT_ALLOWED_MIME_TYPES: readonly string[] = [\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"application/pdf\",\n];\n","import type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig, TransferChannel } from \"./config.js\";\nimport { DEFAULT_MAX_PROOF_BYTES, DEFAULT_ALLOWED_MIME_TYPES } from \"./config.js\";\n\nexport interface ProofInput {\n orderRef: string;\n /** `id` del canal (config.channels) por el que se pagó. */\n channelId: string;\n url: string;\n mimeType: string;\n sizeBytes: number;\n /** Número de transacción/documento del banco. */\n referenceNumber: string;\n declaredAmount: number;\n depositedAt?: string;\n}\n\nexport type ProofRejection =\n | \"order-not-found\"\n | \"order-not-awaiting-proof\"\n | \"unknown-channel\"\n | \"unsupported-file\"\n | \"amount-below-total\"\n | \"duplicate-reference\";\n\nexport type ProofCheck = { ok: true } | { ok: false; reason: ProofRejection; message: string };\n\nexport interface ProofCheckContext {\n order?: Order;\n referenceAlreadyUsed: boolean;\n config: BankTransferConfig;\n /** Canales habilitados (`enabled !== false`) contra los que se valida `channelId`. */\n channels: TransferChannel[];\n}\n\nconst money = (n: number) => `$${n.toFixed(2)}`;\nconst megabytes = (bytes: number) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\nexport function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck {\n const { order, referenceAlreadyUsed, config, channels } = ctx;\n\n if (!order) {\n return {\n ok: false,\n reason: \"order-not-found\",\n message: `No existe una orden con referencia \"${input.orderRef}\". Verifica el código de orden.`,\n };\n }\n\n if (order.status !== \"awaiting_payment_proof\") {\n return {\n ok: false,\n reason: \"order-not-awaiting-proof\",\n message: `La orden ${order.id} está en estado \"${order.status}\" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`,\n };\n }\n\n if (!channels.some((channel) => channel.id === input.channelId)) {\n const enabledIds = channels.map((channel) => channel.id).join(\", \") || \"ninguno\";\n return {\n ok: false,\n reason: \"unknown-channel\",\n message: `El canal de cobro \"${input.channelId}\" no existe o está deshabilitado. Canales habilitados: ${enabledIds}.`,\n };\n }\n\n const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;\n const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;\n if (\n !allowedTypes.includes(input.mimeType) ||\n input.sizeBytes <= 0 ||\n input.sizeBytes > maxBytes\n ) {\n return {\n ok: false,\n reason: \"unsupported-file\",\n message: `El archivo debe ser ${allowedTypes.join(\", \")} y pesar hasta ${megabytes(maxBytes)}. Sube el comprobante en un formato válido.`,\n };\n }\n\n if (input.declaredAmount < order.total) {\n return {\n ok: false,\n reason: \"amount-below-total\",\n message: `El monto declarado (${money(input.declaredAmount)}) es menor al total de la orden (${money(order.total)}). Sube un comprobante por el total.`,\n };\n }\n\n if (referenceAlreadyUsed) {\n return {\n ok: false,\n reason: \"duplicate-reference\",\n message: `El número de referencia \"${input.referenceNumber}\" ya fue usado en otra orden. Cada comprobante debe tener una referencia única.`,\n };\n }\n\n return { ok: true };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita dep de workspace en runtime).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig, TransferChannel } from \"./config.js\";\nimport type { ProofCheck, ProofInput } from \"./proof.js\";\nimport { validateProof } from \"./proof.js\";\n\nfunction enabledChannels(config: BankTransferConfig): TransferChannel[] {\n return config.channels.filter((channel) => channel.enabled !== false);\n}\n\nfunction assertNoDuplicateChannelIds(channels: TransferChannel[]): void {\n const seen = new Set<string>();\n for (const channel of channels) {\n if (seen.has(channel.id)) {\n throw new Error(\n `Configuración de payment-bank-transfer inválida: el id de canal \"${channel.id}\" está repetido en channels.`,\n );\n }\n seen.add(channel.id);\n }\n}\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\n\n/**\n * Contrato de handleWebhook (§4 del brief): \"esta orden se pagó, finalízala\" solo cuando\n * handled === true. Un rechazo NO es un fallo silencioso: viaja como\n * { handled: false, orderId, decision: \"rejected\" }, no como { handled: false } a secas.\n */\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n decision?: \"approved\" | \"rejected\";\n}\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\nexport interface BankTransferPaymentMethod extends PaymentMethod {\n /** Valida y registra el comprobante subido por el cliente para una orden. */\n submitProof(input: ProofInput): Promise<ProofCheck>;\n}\n\nexport interface BankTransferWebhookPayload {\n orderRef?: string;\n decision?: \"approved\" | \"rejected\";\n reviewerId?: string;\n reviewedAt?: string;\n}\n\nexport interface BankTransferPorts {\n findOrder(orderRef: string): Promise<Order | undefined>;\n isReferenceUsed(referenceNumber: string): Promise<boolean>;\n onProofAccepted(input: ProofInput, order: Order): Promise<void>;\n /** Decisión del admin para una orden; ausente/undefined mientras esté en revisión. */\n getDecision?(orderRef: string): Promise<\"approved\" | \"rejected\" | undefined>;\n}\n\nfunction buildWhatsappUrl(whatsapp: string, orderId: string): string {\n const text = encodeURIComponent(`Hola, envío el comprobante de transferencia de la orden ${orderId}.`);\n return `https://wa.me/${whatsapp}?text=${text}`;\n}\n\nexport function toBankTransferPaymentMethod(\n config: BankTransferConfig,\n ports: BankTransferPorts,\n): BankTransferPaymentMethod {\n assertNoDuplicateChannelIds(config.channels);\n\n return {\n id: \"bank-transfer\",\n label: \"Transferencia bancaria\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const channels = enabledChannels(config);\n if (channels.length === 0) {\n throw new Error(\n \"No hay ningún canal de cobro habilitado en la configuración (channels): revisa que al menos uno tenga enabled !== false.\",\n );\n }\n return {\n channels,\n reference: order.id,\n amount: order.total,\n status: \"awaiting_payment_proof\",\n ...(config.whatsapp ? { whatsappUrl: buildWhatsappUrl(config.whatsapp, order.id) } : {}),\n };\n },\n\n async submitProof(input: ProofInput): Promise<ProofCheck> {\n const [order, referenceAlreadyUsed] = await Promise.all([\n ports.findOrder(input.orderRef),\n ports.isReferenceUsed(input.referenceNumber),\n ]);\n\n const check = validateProof(input, {\n order,\n referenceAlreadyUsed,\n config,\n channels: enabledChannels(config),\n });\n if (check.ok && order) {\n await ports.onProofAccepted(input, order);\n }\n return check;\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n const decision = await ports.getDecision?.(ref);\n if (decision === \"approved\") return { status: \"paid\" };\n if (decision === \"rejected\") return { status: \"failed\", error: \"Comprobante rechazado\" };\n return { status: \"pending\" };\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as BankTransferWebhookPayload | null;\n if (!p?.orderRef || (p.decision !== \"approved\" && p.decision !== \"rejected\")) {\n return { handled: false };\n }\n const { orderRef, decision } = p;\n\n if (decision === \"rejected\") {\n return { handled: false, orderId: orderRef, decision };\n }\n\n return { handled: true, orderId: orderRef, decision };\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -6,8 +6,29 @@ interface BankAccount {
|
|
|
6
6
|
/** Cédula/RUC del titular. */
|
|
7
7
|
taxId: string;
|
|
8
8
|
}
|
|
9
|
+
interface ChannelBase {
|
|
10
|
+
/** Único dentro de la configuración. Viaja en el comprobante. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Lo que ve el comprador. Ej. "Banco Pichincha — ahorros". */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Default: true. Se apaga sin borrar el canal de la configuración. */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
type TransferChannel = (ChannelBase & {
|
|
18
|
+
kind: "bank";
|
|
19
|
+
account: BankAccount;
|
|
20
|
+
}) | (ChannelBase & {
|
|
21
|
+
kind: "wallet-qr";
|
|
22
|
+
wallet: "deuna" | "peigo";
|
|
23
|
+
/** Imagen del QR ESTÁTICO del comercio. La sube el comercio; el kit no la genera. */
|
|
24
|
+
qrImageUrl: string;
|
|
25
|
+
/** Titular de la billetera, para que el comprador confirme a quién paga. */
|
|
26
|
+
holder: string;
|
|
27
|
+
/** Teléfono de la billetera, si el comercio también cobra por número. */
|
|
28
|
+
phone?: string;
|
|
29
|
+
});
|
|
9
30
|
interface BankTransferConfig {
|
|
10
|
-
|
|
31
|
+
channels: TransferChannel[];
|
|
11
32
|
/** E.164 sin '+', para el link wa.me. */
|
|
12
33
|
whatsapp?: string;
|
|
13
34
|
maxProofBytes?: number;
|
|
@@ -88,6 +109,8 @@ interface Order {
|
|
|
88
109
|
|
|
89
110
|
interface ProofInput {
|
|
90
111
|
orderRef: string;
|
|
112
|
+
/** `id` del canal (config.channels) por el que se pagó. */
|
|
113
|
+
channelId: string;
|
|
91
114
|
url: string;
|
|
92
115
|
mimeType: string;
|
|
93
116
|
sizeBytes: number;
|
|
@@ -96,7 +119,7 @@ interface ProofInput {
|
|
|
96
119
|
declaredAmount: number;
|
|
97
120
|
depositedAt?: string;
|
|
98
121
|
}
|
|
99
|
-
type ProofRejection = "order-not-found" | "order-not-awaiting-proof" | "unsupported-file" | "amount-below-total" | "duplicate-reference";
|
|
122
|
+
type ProofRejection = "order-not-found" | "order-not-awaiting-proof" | "unknown-channel" | "unsupported-file" | "amount-below-total" | "duplicate-reference";
|
|
100
123
|
type ProofCheck = {
|
|
101
124
|
ok: true;
|
|
102
125
|
} | {
|
|
@@ -108,6 +131,8 @@ interface ProofCheckContext {
|
|
|
108
131
|
order?: Order;
|
|
109
132
|
referenceAlreadyUsed: boolean;
|
|
110
133
|
config: BankTransferConfig;
|
|
134
|
+
/** Canales habilitados (`enabled !== false`) contra los que se valida `channelId`. */
|
|
135
|
+
channels: TransferChannel[];
|
|
111
136
|
}
|
|
112
137
|
declare function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck;
|
|
113
138
|
|
|
@@ -153,4 +178,4 @@ interface BankTransferPorts {
|
|
|
153
178
|
}
|
|
154
179
|
declare function toBankTransferPaymentMethod(config: BankTransferConfig, ports: BankTransferPorts): BankTransferPaymentMethod;
|
|
155
180
|
|
|
156
|
-
export { type BankAccount, type BankTransferConfig, type BankTransferPaymentMethod, type BankTransferPorts, type BankTransferWebhookPayload, DEFAULT_ALLOWED_MIME_TYPES, DEFAULT_MAX_PROOF_BYTES, type ProofCheck, type ProofCheckContext, type ProofInput, type ProofRejection, toBankTransferPaymentMethod, validateProof };
|
|
181
|
+
export { type BankAccount, type BankTransferConfig, type BankTransferPaymentMethod, type BankTransferPorts, type BankTransferWebhookPayload, DEFAULT_ALLOWED_MIME_TYPES, DEFAULT_MAX_PROOF_BYTES, type ProofCheck, type ProofCheckContext, type ProofInput, type ProofRejection, type TransferChannel, toBankTransferPaymentMethod, validateProof };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,29 @@ interface BankAccount {
|
|
|
6
6
|
/** Cédula/RUC del titular. */
|
|
7
7
|
taxId: string;
|
|
8
8
|
}
|
|
9
|
+
interface ChannelBase {
|
|
10
|
+
/** Único dentro de la configuración. Viaja en el comprobante. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Lo que ve el comprador. Ej. "Banco Pichincha — ahorros". */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Default: true. Se apaga sin borrar el canal de la configuración. */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
type TransferChannel = (ChannelBase & {
|
|
18
|
+
kind: "bank";
|
|
19
|
+
account: BankAccount;
|
|
20
|
+
}) | (ChannelBase & {
|
|
21
|
+
kind: "wallet-qr";
|
|
22
|
+
wallet: "deuna" | "peigo";
|
|
23
|
+
/** Imagen del QR ESTÁTICO del comercio. La sube el comercio; el kit no la genera. */
|
|
24
|
+
qrImageUrl: string;
|
|
25
|
+
/** Titular de la billetera, para que el comprador confirme a quién paga. */
|
|
26
|
+
holder: string;
|
|
27
|
+
/** Teléfono de la billetera, si el comercio también cobra por número. */
|
|
28
|
+
phone?: string;
|
|
29
|
+
});
|
|
9
30
|
interface BankTransferConfig {
|
|
10
|
-
|
|
31
|
+
channels: TransferChannel[];
|
|
11
32
|
/** E.164 sin '+', para el link wa.me. */
|
|
12
33
|
whatsapp?: string;
|
|
13
34
|
maxProofBytes?: number;
|
|
@@ -88,6 +109,8 @@ interface Order {
|
|
|
88
109
|
|
|
89
110
|
interface ProofInput {
|
|
90
111
|
orderRef: string;
|
|
112
|
+
/** `id` del canal (config.channels) por el que se pagó. */
|
|
113
|
+
channelId: string;
|
|
91
114
|
url: string;
|
|
92
115
|
mimeType: string;
|
|
93
116
|
sizeBytes: number;
|
|
@@ -96,7 +119,7 @@ interface ProofInput {
|
|
|
96
119
|
declaredAmount: number;
|
|
97
120
|
depositedAt?: string;
|
|
98
121
|
}
|
|
99
|
-
type ProofRejection = "order-not-found" | "order-not-awaiting-proof" | "unsupported-file" | "amount-below-total" | "duplicate-reference";
|
|
122
|
+
type ProofRejection = "order-not-found" | "order-not-awaiting-proof" | "unknown-channel" | "unsupported-file" | "amount-below-total" | "duplicate-reference";
|
|
100
123
|
type ProofCheck = {
|
|
101
124
|
ok: true;
|
|
102
125
|
} | {
|
|
@@ -108,6 +131,8 @@ interface ProofCheckContext {
|
|
|
108
131
|
order?: Order;
|
|
109
132
|
referenceAlreadyUsed: boolean;
|
|
110
133
|
config: BankTransferConfig;
|
|
134
|
+
/** Canales habilitados (`enabled !== false`) contra los que se valida `channelId`. */
|
|
135
|
+
channels: TransferChannel[];
|
|
111
136
|
}
|
|
112
137
|
declare function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck;
|
|
113
138
|
|
|
@@ -153,4 +178,4 @@ interface BankTransferPorts {
|
|
|
153
178
|
}
|
|
154
179
|
declare function toBankTransferPaymentMethod(config: BankTransferConfig, ports: BankTransferPorts): BankTransferPaymentMethod;
|
|
155
180
|
|
|
156
|
-
export { type BankAccount, type BankTransferConfig, type BankTransferPaymentMethod, type BankTransferPorts, type BankTransferWebhookPayload, DEFAULT_ALLOWED_MIME_TYPES, DEFAULT_MAX_PROOF_BYTES, type ProofCheck, type ProofCheckContext, type ProofInput, type ProofRejection, toBankTransferPaymentMethod, validateProof };
|
|
181
|
+
export { type BankAccount, type BankTransferConfig, type BankTransferPaymentMethod, type BankTransferPorts, type BankTransferWebhookPayload, DEFAULT_ALLOWED_MIME_TYPES, DEFAULT_MAX_PROOF_BYTES, type ProofCheck, type ProofCheckContext, type ProofInput, type ProofRejection, type TransferChannel, toBankTransferPaymentMethod, validateProof };
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ var DEFAULT_ALLOWED_MIME_TYPES = [
|
|
|
11
11
|
var money = (n) => `$${n.toFixed(2)}`;
|
|
12
12
|
var megabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
13
13
|
function validateProof(input, ctx) {
|
|
14
|
-
const { order, referenceAlreadyUsed, config } = ctx;
|
|
14
|
+
const { order, referenceAlreadyUsed, config, channels } = ctx;
|
|
15
15
|
if (!order) {
|
|
16
16
|
return {
|
|
17
17
|
ok: false,
|
|
@@ -26,6 +26,14 @@ function validateProof(input, ctx) {
|
|
|
26
26
|
message: `La orden ${order.id} est\xE1 en estado "${order.status}" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
if (!channels.some((channel) => channel.id === input.channelId)) {
|
|
30
|
+
const enabledIds = channels.map((channel) => channel.id).join(", ") || "ninguno";
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: "unknown-channel",
|
|
34
|
+
message: `El canal de cobro "${input.channelId}" no existe o est\xE1 deshabilitado. Canales habilitados: ${enabledIds}.`
|
|
35
|
+
};
|
|
36
|
+
}
|
|
29
37
|
const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;
|
|
30
38
|
const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;
|
|
31
39
|
if (!allowedTypes.includes(input.mimeType) || input.sizeBytes <= 0 || input.sizeBytes > maxBytes) {
|
|
@@ -53,17 +61,38 @@ function validateProof(input, ctx) {
|
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// src/adapter.ts
|
|
64
|
+
function enabledChannels(config) {
|
|
65
|
+
return config.channels.filter((channel) => channel.enabled !== false);
|
|
66
|
+
}
|
|
67
|
+
function assertNoDuplicateChannelIds(channels) {
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const channel of channels) {
|
|
70
|
+
if (seen.has(channel.id)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Configuraci\xF3n de payment-bank-transfer inv\xE1lida: el id de canal "${channel.id}" est\xE1 repetido en channels.`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
seen.add(channel.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
56
78
|
function buildWhatsappUrl(whatsapp, orderId) {
|
|
57
79
|
const text = encodeURIComponent(`Hola, env\xEDo el comprobante de transferencia de la orden ${orderId}.`);
|
|
58
80
|
return `https://wa.me/${whatsapp}?text=${text}`;
|
|
59
81
|
}
|
|
60
82
|
function toBankTransferPaymentMethod(config, ports) {
|
|
83
|
+
assertNoDuplicateChannelIds(config.channels);
|
|
61
84
|
return {
|
|
62
85
|
id: "bank-transfer",
|
|
63
86
|
label: "Transferencia bancaria",
|
|
64
87
|
async prepare(order) {
|
|
88
|
+
const channels = enabledChannels(config);
|
|
89
|
+
if (channels.length === 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"No hay ning\xFAn canal de cobro habilitado en la configuraci\xF3n (channels): revisa que al menos uno tenga enabled !== false."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
65
94
|
return {
|
|
66
|
-
|
|
95
|
+
channels,
|
|
67
96
|
reference: order.id,
|
|
68
97
|
amount: order.total,
|
|
69
98
|
status: "awaiting_payment_proof",
|
|
@@ -75,7 +104,12 @@ function toBankTransferPaymentMethod(config, ports) {
|
|
|
75
104
|
ports.findOrder(input.orderRef),
|
|
76
105
|
ports.isReferenceUsed(input.referenceNumber)
|
|
77
106
|
]);
|
|
78
|
-
const check = validateProof(input, {
|
|
107
|
+
const check = validateProof(input, {
|
|
108
|
+
order,
|
|
109
|
+
referenceAlreadyUsed,
|
|
110
|
+
config,
|
|
111
|
+
channels: enabledChannels(config)
|
|
112
|
+
});
|
|
79
113
|
if (check.ok && order) {
|
|
80
114
|
await ports.onProofAccepted(input, order);
|
|
81
115
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/proof.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface BankAccount {\n bank: string;\n holder: string;\n accountNumber: string;\n accountType: \"ahorros\" | \"corriente\";\n /** Cédula/RUC del titular. */\n taxId: string;\n}\n\nexport interface BankTransferConfig {\n accounts: BankAccount[];\n /** E.164 sin '+', para el link wa.me. */\n whatsapp?: string;\n maxProofBytes?: number;\n allowedMimeTypes?: readonly string[];\n}\n\nexport const DEFAULT_MAX_PROOF_BYTES = 5 * 1024 * 1024;\nexport const DEFAULT_ALLOWED_MIME_TYPES: readonly string[] = [\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"application/pdf\",\n];\n","import type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig } from \"./config.js\";\nimport { DEFAULT_MAX_PROOF_BYTES, DEFAULT_ALLOWED_MIME_TYPES } from \"./config.js\";\n\nexport interface ProofInput {\n orderRef: string;\n url: string;\n mimeType: string;\n sizeBytes: number;\n /** Número de transacción/documento del banco. */\n referenceNumber: string;\n declaredAmount: number;\n depositedAt?: string;\n}\n\nexport type ProofRejection =\n | \"order-not-found\"\n | \"order-not-awaiting-proof\"\n | \"unsupported-file\"\n | \"amount-below-total\"\n | \"duplicate-reference\";\n\nexport type ProofCheck = { ok: true } | { ok: false; reason: ProofRejection; message: string };\n\nexport interface ProofCheckContext {\n order?: Order;\n referenceAlreadyUsed: boolean;\n config: BankTransferConfig;\n}\n\nconst money = (n: number) => `$${n.toFixed(2)}`;\nconst megabytes = (bytes: number) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\nexport function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck {\n const { order, referenceAlreadyUsed, config } = ctx;\n\n if (!order) {\n return {\n ok: false,\n reason: \"order-not-found\",\n message: `No existe una orden con referencia \"${input.orderRef}\". Verifica el código de orden.`,\n };\n }\n\n if (order.status !== \"awaiting_payment_proof\") {\n return {\n ok: false,\n reason: \"order-not-awaiting-proof\",\n message: `La orden ${order.id} está en estado \"${order.status}\" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`,\n };\n }\n\n const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;\n const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;\n if (\n !allowedTypes.includes(input.mimeType) ||\n input.sizeBytes <= 0 ||\n input.sizeBytes > maxBytes\n ) {\n return {\n ok: false,\n reason: \"unsupported-file\",\n message: `El archivo debe ser ${allowedTypes.join(\", \")} y pesar hasta ${megabytes(maxBytes)}. Sube el comprobante en un formato válido.`,\n };\n }\n\n if (input.declaredAmount < order.total) {\n return {\n ok: false,\n reason: \"amount-below-total\",\n message: `El monto declarado (${money(input.declaredAmount)}) es menor al total de la orden (${money(order.total)}). Sube un comprobante por el total.`,\n };\n }\n\n if (referenceAlreadyUsed) {\n return {\n ok: false,\n reason: \"duplicate-reference\",\n message: `El número de referencia \"${input.referenceNumber}\" ya fue usado en otra orden. Cada comprobante debe tener una referencia única.`,\n };\n }\n\n return { ok: true };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita dep de workspace en runtime).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig } from \"./config.js\";\nimport type { ProofCheck, ProofInput } from \"./proof.js\";\nimport { validateProof } from \"./proof.js\";\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\n\n/**\n * Contrato de handleWebhook (§4 del brief): \"esta orden se pagó, finalízala\" solo cuando\n * handled === true. Un rechazo NO es un fallo silencioso: viaja como\n * { handled: false, orderId, decision: \"rejected\" }, no como { handled: false } a secas.\n */\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n decision?: \"approved\" | \"rejected\";\n}\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\nexport interface BankTransferPaymentMethod extends PaymentMethod {\n /** Valida y registra el comprobante subido por el cliente para una orden. */\n submitProof(input: ProofInput): Promise<ProofCheck>;\n}\n\nexport interface BankTransferWebhookPayload {\n orderRef?: string;\n decision?: \"approved\" | \"rejected\";\n reviewerId?: string;\n reviewedAt?: string;\n}\n\nexport interface BankTransferPorts {\n findOrder(orderRef: string): Promise<Order | undefined>;\n isReferenceUsed(referenceNumber: string): Promise<boolean>;\n onProofAccepted(input: ProofInput, order: Order): Promise<void>;\n /** Decisión del admin para una orden; ausente/undefined mientras esté en revisión. */\n getDecision?(orderRef: string): Promise<\"approved\" | \"rejected\" | undefined>;\n}\n\nfunction buildWhatsappUrl(whatsapp: string, orderId: string): string {\n const text = encodeURIComponent(`Hola, envío el comprobante de transferencia de la orden ${orderId}.`);\n return `https://wa.me/${whatsapp}?text=${text}`;\n}\n\nexport function toBankTransferPaymentMethod(\n config: BankTransferConfig,\n ports: BankTransferPorts,\n): BankTransferPaymentMethod {\n return {\n id: \"bank-transfer\",\n label: \"Transferencia bancaria\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n return {\n accounts: config.accounts,\n reference: order.id,\n amount: order.total,\n status: \"awaiting_payment_proof\",\n ...(config.whatsapp ? { whatsappUrl: buildWhatsappUrl(config.whatsapp, order.id) } : {}),\n };\n },\n\n async submitProof(input: ProofInput): Promise<ProofCheck> {\n const [order, referenceAlreadyUsed] = await Promise.all([\n ports.findOrder(input.orderRef),\n ports.isReferenceUsed(input.referenceNumber),\n ]);\n\n const check = validateProof(input, { order, referenceAlreadyUsed, config });\n if (check.ok && order) {\n await ports.onProofAccepted(input, order);\n }\n return check;\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n const decision = await ports.getDecision?.(ref);\n if (decision === \"approved\") return { status: \"paid\" };\n if (decision === \"rejected\") return { status: \"failed\", error: \"Comprobante rechazado\" };\n return { status: \"pending\" };\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as BankTransferWebhookPayload | null;\n if (!p?.orderRef || (p.decision !== \"approved\" && p.decision !== \"rejected\")) {\n return { handled: false };\n }\n const { orderRef, decision } = p;\n\n if (decision === \"rejected\") {\n return { handled: false, orderId: orderRef, decision };\n }\n\n return { handled: true, orderId: orderRef, decision };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/proof.ts","../src/adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface BankAccount {\n bank: string;\n holder: string;\n accountNumber: string;\n accountType: \"ahorros\" | \"corriente\";\n /** Cédula/RUC del titular. */\n taxId: string;\n}\n\ninterface ChannelBase {\n /** Único dentro de la configuración. Viaja en el comprobante. */\n id: string;\n /** Lo que ve el comprador. Ej. \"Banco Pichincha — ahorros\". */\n label: string;\n /** Default: true. Se apaga sin borrar el canal de la configuración. */\n enabled?: boolean;\n}\n\nexport type TransferChannel =\n | (ChannelBase & { kind: \"bank\"; account: BankAccount })\n | (ChannelBase & {\n kind: \"wallet-qr\";\n wallet: \"deuna\" | \"peigo\";\n // ponytail: QR estático sin monto — se cambia a QR dinámico cuando un comercio tenga contrato de API\n /** Imagen del QR ESTÁTICO del comercio. La sube el comercio; el kit no la genera. */\n qrImageUrl: string;\n /** Titular de la billetera, para que el comprador confirme a quién paga. */\n holder: string;\n /** Teléfono de la billetera, si el comercio también cobra por número. */\n phone?: string;\n });\n\nexport interface BankTransferConfig {\n channels: TransferChannel[];\n /** E.164 sin '+', para el link wa.me. */\n whatsapp?: string;\n maxProofBytes?: number;\n allowedMimeTypes?: readonly string[];\n}\n\nexport const DEFAULT_MAX_PROOF_BYTES = 5 * 1024 * 1024;\nexport const DEFAULT_ALLOWED_MIME_TYPES: readonly string[] = [\n \"image/jpeg\",\n \"image/png\",\n \"image/webp\",\n \"application/pdf\",\n];\n","import type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig, TransferChannel } from \"./config.js\";\nimport { DEFAULT_MAX_PROOF_BYTES, DEFAULT_ALLOWED_MIME_TYPES } from \"./config.js\";\n\nexport interface ProofInput {\n orderRef: string;\n /** `id` del canal (config.channels) por el que se pagó. */\n channelId: string;\n url: string;\n mimeType: string;\n sizeBytes: number;\n /** Número de transacción/documento del banco. */\n referenceNumber: string;\n declaredAmount: number;\n depositedAt?: string;\n}\n\nexport type ProofRejection =\n | \"order-not-found\"\n | \"order-not-awaiting-proof\"\n | \"unknown-channel\"\n | \"unsupported-file\"\n | \"amount-below-total\"\n | \"duplicate-reference\";\n\nexport type ProofCheck = { ok: true } | { ok: false; reason: ProofRejection; message: string };\n\nexport interface ProofCheckContext {\n order?: Order;\n referenceAlreadyUsed: boolean;\n config: BankTransferConfig;\n /** Canales habilitados (`enabled !== false`) contra los que se valida `channelId`. */\n channels: TransferChannel[];\n}\n\nconst money = (n: number) => `$${n.toFixed(2)}`;\nconst megabytes = (bytes: number) => `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\nexport function validateProof(input: ProofInput, ctx: ProofCheckContext): ProofCheck {\n const { order, referenceAlreadyUsed, config, channels } = ctx;\n\n if (!order) {\n return {\n ok: false,\n reason: \"order-not-found\",\n message: `No existe una orden con referencia \"${input.orderRef}\". Verifica el código de orden.`,\n };\n }\n\n if (order.status !== \"awaiting_payment_proof\") {\n return {\n ok: false,\n reason: \"order-not-awaiting-proof\",\n message: `La orden ${order.id} está en estado \"${order.status}\" y no admite un nuevo comprobante. Si ya pagaste, revisa el estado de tu pedido.`,\n };\n }\n\n if (!channels.some((channel) => channel.id === input.channelId)) {\n const enabledIds = channels.map((channel) => channel.id).join(\", \") || \"ninguno\";\n return {\n ok: false,\n reason: \"unknown-channel\",\n message: `El canal de cobro \"${input.channelId}\" no existe o está deshabilitado. Canales habilitados: ${enabledIds}.`,\n };\n }\n\n const maxBytes = config.maxProofBytes ?? DEFAULT_MAX_PROOF_BYTES;\n const allowedTypes = config.allowedMimeTypes ?? DEFAULT_ALLOWED_MIME_TYPES;\n if (\n !allowedTypes.includes(input.mimeType) ||\n input.sizeBytes <= 0 ||\n input.sizeBytes > maxBytes\n ) {\n return {\n ok: false,\n reason: \"unsupported-file\",\n message: `El archivo debe ser ${allowedTypes.join(\", \")} y pesar hasta ${megabytes(maxBytes)}. Sube el comprobante en un formato válido.`,\n };\n }\n\n if (input.declaredAmount < order.total) {\n return {\n ok: false,\n reason: \"amount-below-total\",\n message: `El monto declarado (${money(input.declaredAmount)}) es menor al total de la orden (${money(order.total)}). Sube un comprobante por el total.`,\n };\n }\n\n if (referenceAlreadyUsed) {\n return {\n ok: false,\n reason: \"duplicate-reference\",\n message: `El número de referencia \"${input.referenceNumber}\" ya fue usado en otra orden. Cada comprobante debe tener una referencia única.`,\n };\n }\n\n return { ok: true };\n}\n","// Tipos estructuralmente compatibles con payment-core (sin importarlo — evita dep de workspace en runtime).\nimport type { Order } from \"@pandait.tech/commerce-core\";\nimport type { BankTransferConfig, TransferChannel } from \"./config.js\";\nimport type { ProofCheck, ProofInput } from \"./proof.js\";\nimport { validateProof } from \"./proof.js\";\n\nfunction enabledChannels(config: BankTransferConfig): TransferChannel[] {\n return config.channels.filter((channel) => channel.enabled !== false);\n}\n\nfunction assertNoDuplicateChannelIds(channels: TransferChannel[]): void {\n const seen = new Set<string>();\n for (const channel of channels) {\n if (seen.has(channel.id)) {\n throw new Error(\n `Configuración de payment-bank-transfer inválida: el id de canal \"${channel.id}\" está repetido en channels.`,\n );\n }\n seen.add(channel.id);\n }\n}\n\ninterface ConfirmResult {\n status: \"paid\" | \"pending\" | \"failed\" | \"challenge\";\n transactionId?: string;\n redirectUrl?: string;\n error?: string;\n}\n\n/**\n * Contrato de handleWebhook (§4 del brief): \"esta orden se pagó, finalízala\" solo cuando\n * handled === true. Un rechazo NO es un fallo silencioso: viaja como\n * { handled: false, orderId, decision: \"rejected\" }, no como { handled: false } a secas.\n */\ninterface WebhookResult {\n handled: boolean;\n orderId?: string;\n decision?: \"approved\" | \"rejected\";\n}\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\nexport interface BankTransferPaymentMethod extends PaymentMethod {\n /** Valida y registra el comprobante subido por el cliente para una orden. */\n submitProof(input: ProofInput): Promise<ProofCheck>;\n}\n\nexport interface BankTransferWebhookPayload {\n orderRef?: string;\n decision?: \"approved\" | \"rejected\";\n reviewerId?: string;\n reviewedAt?: string;\n}\n\nexport interface BankTransferPorts {\n findOrder(orderRef: string): Promise<Order | undefined>;\n isReferenceUsed(referenceNumber: string): Promise<boolean>;\n onProofAccepted(input: ProofInput, order: Order): Promise<void>;\n /** Decisión del admin para una orden; ausente/undefined mientras esté en revisión. */\n getDecision?(orderRef: string): Promise<\"approved\" | \"rejected\" | undefined>;\n}\n\nfunction buildWhatsappUrl(whatsapp: string, orderId: string): string {\n const text = encodeURIComponent(`Hola, envío el comprobante de transferencia de la orden ${orderId}.`);\n return `https://wa.me/${whatsapp}?text=${text}`;\n}\n\nexport function toBankTransferPaymentMethod(\n config: BankTransferConfig,\n ports: BankTransferPorts,\n): BankTransferPaymentMethod {\n assertNoDuplicateChannelIds(config.channels);\n\n return {\n id: \"bank-transfer\",\n label: \"Transferencia bancaria\",\n\n async prepare(order: Order): Promise<Record<string, unknown>> {\n const channels = enabledChannels(config);\n if (channels.length === 0) {\n throw new Error(\n \"No hay ningún canal de cobro habilitado en la configuración (channels): revisa que al menos uno tenga enabled !== false.\",\n );\n }\n return {\n channels,\n reference: order.id,\n amount: order.total,\n status: \"awaiting_payment_proof\",\n ...(config.whatsapp ? { whatsappUrl: buildWhatsappUrl(config.whatsapp, order.id) } : {}),\n };\n },\n\n async submitProof(input: ProofInput): Promise<ProofCheck> {\n const [order, referenceAlreadyUsed] = await Promise.all([\n ports.findOrder(input.orderRef),\n ports.isReferenceUsed(input.referenceNumber),\n ]);\n\n const check = validateProof(input, {\n order,\n referenceAlreadyUsed,\n config,\n channels: enabledChannels(config),\n });\n if (check.ok && order) {\n await ports.onProofAccepted(input, order);\n }\n return check;\n },\n\n async confirm(ref: string): Promise<ConfirmResult> {\n const decision = await ports.getDecision?.(ref);\n if (decision === \"approved\") return { status: \"paid\" };\n if (decision === \"rejected\") return { status: \"failed\", error: \"Comprobante rechazado\" };\n return { status: \"pending\" };\n },\n\n async handleWebhook(payload: unknown): Promise<WebhookResult> {\n const p = payload as BankTransferWebhookPayload | null;\n if (!p?.orderRef || (p.decision !== \"approved\" && p.decision !== \"rejected\")) {\n return { handled: false };\n }\n const { orderRef, decision } = p;\n\n if (decision === \"rejected\") {\n return { handled: false, orderId: orderRef, decision };\n }\n\n return { handled: true, orderId: orderRef, decision };\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pandait.tech/payment-bank-transfer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Transferencia bancaria con comprobante para panda-commerce-kit. Implementa PaymentMethod sin red: datos de cuentas + validación de comprobante + aprobación manual del admin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typescript": "^5.6.3",
|
|
46
46
|
"vitest": "^4.1.7",
|
|
47
47
|
"@pandait.tech/commerce-core": "0.1.0",
|
|
48
|
-
"@pandait.tech/payment-core": "0.0.
|
|
48
|
+
"@pandait.tech/payment-core": "0.0.3"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsup",
|