Merge fix/epayco-pse-reemplaza-payu into main
Build and Push Medusa / Escaneo de Secretos (Gitleaks) (push) Successful in 30s
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Medusa / Construir y publicar Medusa (push) Failing after 4m19s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 18m42s
Build and Push Medusa / Escaneo de Secretos (Gitleaks) (push) Successful in 30s
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Medusa / Construir y publicar Medusa (push) Failing after 4m19s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 18m42s
This commit is contained in:
commit
d4716382d3
17 files changed
+471
-509
No files matched your search
@@ -2,7 +2,7 @@ import { createPaymentCollection, createPsePaymentSession } from "@/lib/headless
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { cartId, ...pse } = await request.json();
|
||||
if (!cartId || !pse.bankCode || !pse.docNumber || !pse.fullName || !pse.email || !pse.phone) {
|
||||
if (!cartId || !pse.bankCode || !pse.docNumber || !pse.firstName || !pse.lastName || !pse.email || !pse.phone) {
|
||||
return Response.json({ error: "Faltan campos obligatorios para el pago con PSE" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -25,10 +25,11 @@ data:
|
||||
# MEILI_MASTER_KEY en secrets.template.yaml).
|
||||
MEILISEARCH_HOST: http://meilisearch-svc:7700
|
||||
|
||||
# PayU LatAm (ADR 0009) -- solo URLs/config no sensible, credenciales
|
||||
# reales (apiLogin/apiKey/merchantId/accountId) van en
|
||||
# secrets.template.yaml. PAYU_ENV=sandbox hasta que haya credenciales
|
||||
# ePayco (ADR 0009 -- tercer proveedor evaluado, ver medusa-config.ts
|
||||
# para el historial completo) -- solo URLs/config no sensible,
|
||||
# credenciales reales (publicKey/privateKey/pCustIdCliente/pKey) van en
|
||||
# secrets.template.yaml. EPAYCO_ENV=sandbox hasta que haya credenciales
|
||||
# de produccion reales.
|
||||
PAYU_ENV: sandbox
|
||||
PAYU_RESPONSE_URL: https://shop.cruzcloud.net/checkout/pse/return
|
||||
PAYU_NOTIFY_URL: https://commerce.cruzcloud.net/hooks/payment/payu_pse
|
||||
EPAYCO_ENV: sandbox
|
||||
EPAYCO_RESPONSE_URL: https://shop.cruzcloud.net/checkout/pse/return
|
||||
EPAYCO_NOTIFY_URL: https://commerce.cruzcloud.net/hooks/payment/epayco_pse
|
||||
@@ -36,14 +36,17 @@ stringData:
|
||||
# x-revalidate-secret al invocar POST /api/revalidate en el storefront.
|
||||
REVALIDATE_SECRET: CAMBIAR
|
||||
|
||||
# PayU LatAm (ADR 0009) -- credenciales del comercio. Mientras no exista
|
||||
# cuenta real, usar las de sandbox genéricas y públicas de PayU
|
||||
# (developers.payulatam.com/latam/en/docs/getting-started/test-your-solution.html),
|
||||
# nunca hardcodeadas acá.
|
||||
PAYU_API_LOGIN: CAMBIAR
|
||||
PAYU_API_KEY: CAMBIAR
|
||||
PAYU_MERCHANT_ID: CAMBIAR
|
||||
PAYU_ACCOUNT_ID: CAMBIAR
|
||||
# ePayco (ADR 0009 -- tercer proveedor evaluado: PayU LatAm quedó
|
||||
# inviable porque Rapyd compró ese negocio en 2023, Rapyd descartado
|
||||
# por costo; ePayco es de Davivienda desde 2024, mejor tarifa PSE para
|
||||
# el volumen real del usuario). Credenciales del comercio real, nunca
|
||||
# hardcodeadas acá. p_cust_id_cliente/p_key son valores propios del
|
||||
# dashboard de ePayco (Configuración > Llaves secretas), distintos de
|
||||
# publicKey/privateKey.
|
||||
EPAYCO_PUBLIC_KEY: CAMBIAR
|
||||
EPAYCO_PRIVATE_KEY: CAMBIAR
|
||||
EPAYCO_P_CUST_ID_CLIENTE: CAMBIAR
|
||||
EPAYCO_P_KEY: CAMBIAR
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
|
||||
@@ -14,8 +14,8 @@ const SELECT_CLASS =
|
||||
"h-12 w-full rounded-full border border-slate-200 bg-white px-5 text-sm text-slate-950 outline-none transition focus:border-cyan-500 focus:ring-4 focus:ring-cyan-300/30 disabled:opacity-50";
|
||||
|
||||
// ADR 0009: direccion + envio + pago real, todo contra el Store API real.
|
||||
// El pago es PayU LatAm/PSE (modulo custom del backend, ver
|
||||
// workloads/commerce-backend/src/modules/payu-pse) -- redirige al banco
|
||||
// El pago es ePayco/PSE (modulo custom del backend, ver
|
||||
// workloads/commerce-backend/src/modules/epayco-pse) -- redirige al banco
|
||||
// del comprador, la confirmacion real llega despues via webhook async
|
||||
// (nunca por esta pantalla). WhatsApp queda como respaldo manual si la
|
||||
// iniciacion del pago falla, ya no como camino principal.
|
||||
@@ -68,7 +68,7 @@ export function CheckoutClient() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [banks, setBanks] = useState<PseBank[]>([]);
|
||||
const [bankCode, setBankCode] = useState("");
|
||||
const [userType, setUserType] = useState<"N" | "J">("N");
|
||||
const [userType, setUserType] = useState<"0" | "1">("0");
|
||||
const [docType, setDocType] = useState<"CC" | "CE" | "NIT" | "TI" | "PP">("CC");
|
||||
const [docNumber, setDocNumber] = useState("");
|
||||
|
||||
@@ -139,17 +139,16 @@ export function CheckoutClient() {
|
||||
userType,
|
||||
docType,
|
||||
docNumber,
|
||||
fullName: `${form.firstName} ${form.lastName}`.trim(),
|
||||
firstName: form.firstName,
|
||||
lastName: form.lastName,
|
||||
email: form.email,
|
||||
phone: form.phone,
|
||||
address: form.address1,
|
||||
city: selectedCiudad.name,
|
||||
state: selectedDepartamento.name,
|
||||
});
|
||||
if (session.error) throw new Error(session.error);
|
||||
const completion = await postJson("/api/cart/complete", { cartId });
|
||||
if (completion.error) throw new Error(completion.error);
|
||||
// Navegacion completa, no iframe -- asi lo exige PayU para el
|
||||
// Navegacion completa, no iframe -- asi lo exige ePayco para el
|
||||
// flujo de PSE. La confirmacion real llega despues via webhook,
|
||||
// esta pagina nunca vuelve a cargar sola.
|
||||
window.location.href = session.bankUrl;
|
||||
@@ -260,8 +259,8 @@ export function CheckoutClient() {
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 text-sm font-bold">
|
||||
<label className="flex items-center gap-2"><input type="radio" name="userType" checked={userType === "N"} onChange={() => setUserType("N")} />Persona natural</label>
|
||||
<label className="flex items-center gap-2"><input type="radio" name="userType" checked={userType === "J"} onChange={() => setUserType("J")} />Persona jurídica</label>
|
||||
<label className="flex items-center gap-2"><input type="radio" name="userType" checked={userType === "0"} onChange={() => setUserType("0")} />Persona natural</label>
|
||||
<label className="flex items-center gap-2"><input type="radio" name="userType" checked={userType === "1"} onChange={() => setUserType("1")} />Persona jurídica</label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
|
||||
@@ -5,12 +5,12 @@ import { Clock } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// PayU redirige aca despues de que el comprador paga en su banco -- esto
|
||||
// NO confirma el pago. Los parametros de esta URL son solo informativos;
|
||||
// el estado real y definitivo llega por el webhook async de PayU al
|
||||
// backend (getWebhookActionAndData en el modulo payu-pse), nunca por
|
||||
// esta pantalla. Por eso el mensaje nunca afirma exito, solo que
|
||||
// estamos esperando la confirmacion.
|
||||
// ePayco redirige aca despues de que el comprador paga en su banco --
|
||||
// esto NO confirma el pago. Los parametros de esta URL son solo
|
||||
// informativos; el estado real y definitivo llega por el webhook async
|
||||
// de ePayco al backend (getWebhookActionAndData en el modulo
|
||||
// epayco-pse), nunca por esta pantalla. Por eso el mensaje nunca afirma
|
||||
// exito, solo que estamos esperando la confirmacion.
|
||||
export function PseReturnClient() {
|
||||
const searchParams = useSearchParams();
|
||||
const referenceCode = searchParams.get("referenceCode") ?? searchParams.get("reference_sale");
|
||||
@@ -20,7 +20,7 @@ export function PseReturnClient() {
|
||||
<Clock className="mx-auto size-12 text-ari-marino" />
|
||||
<h1 className="mt-6 text-2xl font-black">Estamos confirmando tu pago</h1>
|
||||
<p className="mt-3 text-slate-500">
|
||||
Tu banco ya procesó la transferencia. PayU nos avisa en unos minutos si quedó aprobada — te llegará la confirmación por correo.
|
||||
Tu banco ya procesó la transferencia. ePayco nos avisa en unos minutos si quedó aprobada — te llegará la confirmación por correo.
|
||||
{referenceCode && (
|
||||
<>
|
||||
{" "}Tu número de referencia es <strong>{referenceCode}</strong>.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// ADR 0009: pago real via PayU LatAm (PSE), modulo custom del backend
|
||||
// (workloads/commerce-backend/src/modules/payu-pse). Archivo separado de
|
||||
// medusa-cart.ts por el mismo motivo que ese archivo esta separado de
|
||||
// ADR 0009: pago real via ePayco (PSE) -- tercer proveedor evaluado
|
||||
// (PayU LatAm descartado porque Rapyd compró ese negocio en 2023 y el
|
||||
// registro real quedó con credenciales incompatibles; Rapyd descartado
|
||||
// por costo). ePayco: 100% de Davivienda desde 2024, comisión PSE más
|
||||
// baja para el volumen real del usuario, tarifa preferencial con cuenta
|
||||
// Davivienda. Módulo custom del backend
|
||||
// (workloads/commerce-backend/src/modules/epayco-pse). Archivo separado
|
||||
// de medusa-cart.ts por el mismo motivo que ese archivo esta separado de
|
||||
// medusa.ts -- reutiliza su mismo fetch helper (medusaCartFetch), no
|
||||
// duplica la logica de headers/no-store.
|
||||
import { medusaCartFetch } from "./medusa-cart";
|
||||
@@ -10,7 +15,9 @@ export interface PseBank {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type PseUserType = "N" | "J";
|
||||
// "0" persona natural, "1" persona juridica -- valores reales que espera
|
||||
// ePayco (campo type_person), no un codigo propio nuestro.
|
||||
export type PseUserType = "0" | "1";
|
||||
export type PseDocType = "CC" | "CE" | "NIT" | "TI" | "PP";
|
||||
|
||||
export interface PsePaymentInput {
|
||||
@@ -18,18 +25,17 @@ export interface PsePaymentInput {
|
||||
userType: PseUserType;
|
||||
docType: PseDocType;
|
||||
docNumber: string;
|
||||
fullName: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
// "pp_{identifier}_{id}" -- convencion de Medusa, confirmada contra el
|
||||
// codigo fuente de @medusajs/payment (identifier "payu" + id "pse" en
|
||||
// codigo fuente de @medusajs/payment (identifier "epayco" + id "pse" en
|
||||
// medusa-config.ts).
|
||||
const PSE_PROVIDER_ID = "pp_payu_pse";
|
||||
const PSE_PROVIDER_ID = "pp_epayco_pse";
|
||||
|
||||
export async function getPseBanks(): Promise<PseBank[]> {
|
||||
const data = await medusaCartFetch("/store/payments/pse-banks");
|
||||
@@ -58,12 +64,11 @@ export async function createPsePaymentSession(paymentCollectionId: string, input
|
||||
user_type: input.userType,
|
||||
doc_type: input.docType,
|
||||
doc_number: input.docNumber,
|
||||
full_name: input.fullName,
|
||||
first_name: input.firstName,
|
||||
last_name: input.lastName,
|
||||
email: input.email,
|
||||
phone: input.phone,
|
||||
address: input.address,
|
||||
city: input.city,
|
||||
state: input.state,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -73,7 +78,7 @@ export async function createPsePaymentSession(paymentCollectionId: string, input
|
||||
const session = collection.payment_sessions?.find((paymentSession) => paymentSession.provider_id === PSE_PROVIDER_ID);
|
||||
const bankUrl = session?.data?.bank_url;
|
||||
if (!bankUrl) {
|
||||
throw new Error("PayU no devolvió la URL del banco para completar el pago.");
|
||||
throw new Error("ePayco no devolvió la URL del banco para completar el pago.");
|
||||
}
|
||||
return bankUrl;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user