Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a973b2671e | ||
|
|
bdd7c06a32 | ||
|
|
0c356d6fbe | ||
|
|
c507da8abc | ||
|
|
e42d588514 | ||
|
|
e2938248f8 | ||
|
|
4992da394b |
No files matched your search
@@ -33,7 +33,16 @@ ENV CI=true \
|
||||
EPAYCO_P_KEY=build-only-epayco-pkey \
|
||||
EPAYCO_ENV=sandbox \
|
||||
EPAYCO_RESPONSE_URL=http://localhost:8000/checkout/pse/return \
|
||||
EPAYCO_NOTIFY_URL=http://localhost:9000/hooks/payment/epayco_pse
|
||||
EPAYCO_NOTIFY_URL=http://localhost:9000/hooks/payment/epayco_pse \
|
||||
ENVIOCLICK_API_KEY=build-only-envioclick-key \
|
||||
ENVIOCLICK_SANDBOX=true \
|
||||
ENVIOCLICK_ORIGIN_DANE_CODE=11001000 \
|
||||
ENVIOCLICK_ORIGIN_ADDRESS="Calle 1 No 1-1" \
|
||||
ENVIOCLICK_ORIGIN_COMPANY="ARI Shopping" \
|
||||
ENVIOCLICK_ORIGIN_FIRST_NAME=ARI \
|
||||
ENVIOCLICK_ORIGIN_LAST_NAME=Shopping \
|
||||
ENVIOCLICK_ORIGIN_EMAIL=[email protected] \
|
||||
ENVIOCLICK_ORIGIN_PHONE=3000000000
|
||||
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
|
||||
|
||||
@@ -133,6 +133,45 @@ module.exports = defineConfig({
|
||||
},
|
||||
|
||||
modules: [
|
||||
// ADR 0009 §2 / EnvíoClick: defineConfig() ya registra por defecto
|
||||
// un modulo de fulfillment con el provider manual_manual
|
||||
// (confirmado leyendo @medusajs/utils/dist/common/define-config.js
|
||||
// directo) -- pero como este bloque propio comparte el mismo tipo de
|
||||
// modulo, transformModules() lo pisa por completo (indexa por
|
||||
// serviceName, el ultimo que llega gana), no lo mezcla. Por eso
|
||||
// manual_manual va explicito ACA tambien: si se omitiera, la
|
||||
// shipping option existente que ya usa ese provider se rompe en
|
||||
// silencio. envioclick-fulfillment implementa el mismo contrato
|
||||
// (AbstractFulfillmentProviderService) contra
|
||||
// developers.envioclickpro.com.co, verificado en vivo (no solo doc)
|
||||
// con una llamada de prueba real que devolvio tarifas reales de
|
||||
// varias transportadoras.
|
||||
{
|
||||
resolve: "@medusajs/medusa/fulfillment",
|
||||
options: {
|
||||
providers: [
|
||||
{
|
||||
resolve: "@medusajs/medusa/fulfillment-manual",
|
||||
id: "manual",
|
||||
},
|
||||
{
|
||||
resolve: "./src/modules/envioclick-fulfillment",
|
||||
id: "standard",
|
||||
options: {
|
||||
apiKey: required("ENVIOCLICK_API_KEY"),
|
||||
sandbox: process.env.ENVIOCLICK_SANDBOX !== "false",
|
||||
originDaneCode: process.env.ENVIOCLICK_ORIGIN_DANE_CODE || "11001000",
|
||||
originAddress: required("ENVIOCLICK_ORIGIN_ADDRESS"),
|
||||
originCompany: process.env.ENVIOCLICK_ORIGIN_COMPANY || "ARI Shopping",
|
||||
originFirstName: process.env.ENVIOCLICK_ORIGIN_FIRST_NAME || "ARI",
|
||||
originLastName: process.env.ENVIOCLICK_ORIGIN_LAST_NAME || "Shopping",
|
||||
originEmail: required("ENVIOCLICK_ORIGIN_EMAIL"),
|
||||
originPhone: required("ENVIOCLICK_ORIGIN_PHONE"),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resolve: "@medusajs/medusa/file",
|
||||
options: {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// EnvíoClick (ADR 0009 §2, ya anticipado desde el modelo de direcciones
|
||||
// del checkout): cotización y generación de guía real por peso/volumen,
|
||||
// reemplazando la tarifa fija de $15.000 COP. Contrato REST confirmado en
|
||||
// vivo contra la cuenta real del usuario (apiDoc real,
|
||||
// apidoc.envioclickpro.com.co/api_data.json -- la SPA de la doc no
|
||||
// renderiza sin JS, pero ese archivo generado por apidoc trae la spec
|
||||
// completa) y una llamada de prueba real que devolvió 200 con tarifas
|
||||
// reales de PIBOX/COORDINADORA/ENVIA/RAPPI/TCC.
|
||||
|
||||
const BASE_URL = "https://api.envioclickpro.com.co/api/v2";
|
||||
|
||||
export interface EnvioClickCredentials {
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface EnvioClickPackage {
|
||||
weight: number;
|
||||
height: number;
|
||||
length: number;
|
||||
// "widht" es un typo real de la API (confirmado en el body de
|
||||
// /shipment de la doc oficial), no un error nuestro -- solo se usa acá,
|
||||
// en el request de /quotation el campo si se llama "width" (confirmado
|
||||
// tambien contra la doc real, son dos endpoints con esa inconsistencia).
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface EnvioClickRate {
|
||||
idRate: number;
|
||||
idProduct: number;
|
||||
product: string;
|
||||
idCarrier: number;
|
||||
carrier: string;
|
||||
flete: number;
|
||||
minimumInsurance: number;
|
||||
extraInsurance: number;
|
||||
deliveryDays: number;
|
||||
cod: boolean;
|
||||
}
|
||||
|
||||
export interface EnvioClickAddress {
|
||||
daneCode: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
async function envioclickRequest(
|
||||
credentials: EnvioClickCredentials,
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Header real confirmado contra la doc oficial y una llamada en
|
||||
// vivo -- "Authorization" a secas (sin "Bearer"), NO
|
||||
// "AuthorizationKey" como usaba el plugin de WordPress de
|
||||
// referencia (ese era para la API v1, desactualizada).
|
||||
Authorization: credentials.apiKey,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await response.text();
|
||||
let json: Record<string, unknown>;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`Respuesta no-JSON de EnvíoClick (HTTP ${response.status}): ${text.slice(0, 300)}`);
|
||||
}
|
||||
if (json.status !== "OK") {
|
||||
throw new Error(`EnvíoClick respondió con error: ${JSON.stringify(json).slice(0, 400)}`);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
// Solo 1 paquete por cotizacion (limite real documentado y confirmado:
|
||||
// "min 1, max 1" -- cada vez menos transportadoras ofrecen multi-paquete).
|
||||
export async function quoteShipment(
|
||||
credentials: EnvioClickCredentials,
|
||||
params: {
|
||||
pkg: EnvioClickPackage;
|
||||
description: string;
|
||||
contentValue: number;
|
||||
origin: EnvioClickAddress;
|
||||
destination: EnvioClickAddress;
|
||||
},
|
||||
): Promise<EnvioClickRate[]> {
|
||||
const json = await envioclickRequest(credentials, "/quotation", {
|
||||
packages: [params.pkg],
|
||||
description: params.description,
|
||||
contentValue: params.contentValue,
|
||||
origin: params.origin,
|
||||
destination: params.destination,
|
||||
});
|
||||
const data = json.data as Record<string, unknown>;
|
||||
return (data.rates as EnvioClickRate[]) ?? [];
|
||||
}
|
||||
|
||||
export interface EnvioClickPerson {
|
||||
company: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
suburb: string;
|
||||
crossStreet: string;
|
||||
reference: string;
|
||||
daneCode: string;
|
||||
}
|
||||
|
||||
export interface CreateGuideResult {
|
||||
guide: string;
|
||||
url: string;
|
||||
tracker: string;
|
||||
idOrder: number;
|
||||
}
|
||||
|
||||
export async function createGuide(
|
||||
credentials: EnvioClickCredentials,
|
||||
sandbox: boolean,
|
||||
params: {
|
||||
idRate: number;
|
||||
myShipmentReference: string;
|
||||
externalOrderId: string;
|
||||
pkg: EnvioClickPackage;
|
||||
description: string;
|
||||
contentValue: number;
|
||||
origin: EnvioClickPerson;
|
||||
destination: EnvioClickPerson;
|
||||
},
|
||||
): Promise<CreateGuideResult> {
|
||||
// /shipment_sandbox es el UNICO endpoint con modo de pruebas real (la
|
||||
// cotizacion siempre usa tarifas reales de las transportadoras, no
|
||||
// tiene sandbox) -- confirmado en la doc oficial ("Deberás cambiar a
|
||||
// la URL de producción para que puedas generar guías reales").
|
||||
const path = sandbox ? "/shipment_sandbox" : "/shipment";
|
||||
const json = await envioclickRequest(credentials, path, {
|
||||
idRate: params.idRate,
|
||||
myShipmentReference: params.myShipmentReference,
|
||||
external_order_id: params.externalOrderId,
|
||||
requestPickup: false,
|
||||
pickupDate: new Date().toISOString().slice(0, 10),
|
||||
insurance: false,
|
||||
description: params.description,
|
||||
contentValue: params.contentValue,
|
||||
packages: [
|
||||
{
|
||||
weight: params.pkg.weight,
|
||||
height: params.pkg.height,
|
||||
length: params.pkg.length,
|
||||
widht: params.pkg.width,
|
||||
},
|
||||
],
|
||||
origin: params.origin,
|
||||
destination: params.destination,
|
||||
});
|
||||
const data = json.data as Record<string, unknown>;
|
||||
return {
|
||||
guide: String(data.guide ?? ""),
|
||||
url: String(data.url ?? ""),
|
||||
tracker: String(data.tracker ?? ""),
|
||||
idOrder: Number(data.idOrder ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
// Hasta 50 ordenes por llamada (limite real documentado).
|
||||
export async function cancelGuides(credentials: EnvioClickCredentials, idOrders: number[]): Promise<void> {
|
||||
await envioclickRequest(credentials, "cancellation/batch/order", { idOrders });
|
||||
}
|
||||
|
||||
// código DANE estandar (5 digitos: 2 de departamento + 3 de municipio,
|
||||
// ej. 11001 = Bogota, 5001 = Medellin -- mismo id que ya usa
|
||||
// lib/data/colombia.ts del storefront, confirmado contra su propio
|
||||
// comentario) con el padding real que espera EnvioClick (8 caracteres,
|
||||
// confirmado contra su propio ejemplo real: "11001000" para Bogota =
|
||||
// "11001" + "000").
|
||||
export function toEnvioClickDaneCode(divipolaCityId: number): string {
|
||||
return `${String(divipolaCityId).padStart(5, "0")}000`;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ModuleProvider, Modules } from "@medusajs/framework/utils";
|
||||
import EnvioClickFulfillmentService from "./service";
|
||||
|
||||
export default ModuleProvider(Modules.FULFILLMENT, {
|
||||
services: [EnvioClickFulfillmentService],
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils";
|
||||
import type {
|
||||
CalculatedShippingOptionPrice,
|
||||
CalculateShippingOptionPriceContext,
|
||||
CalculateShippingOptionPriceDTO,
|
||||
CreateFulfillmentResult,
|
||||
CreateShippingOptionDTO,
|
||||
FulfillmentDTO,
|
||||
FulfillmentItemDTO,
|
||||
FulfillmentOption,
|
||||
FulfillmentOrderDTO,
|
||||
Logger,
|
||||
ValidateFulfillmentDataContext,
|
||||
} from "@medusajs/framework/types";
|
||||
import { cancelGuides, createGuide, quoteShipment, toEnvioClickDaneCode } from "../../lib/envioclick";
|
||||
import { DEFAULT_PACKAGE, FALLBACK_PRICE_COP, type EnvioClickOptions } from "./types";
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
// Contexto minimo compartido entre calculatePrice/createFulfillment --
|
||||
// ambos necesitan lo mismo (id DIVIPOLA de la ciudad destino, guardado
|
||||
// como metadata en la direccion por el checkout -- ver ADR 0009 y
|
||||
// checkout-client.tsx del storefront) y el paquete agregado del pedido.
|
||||
interface AddressLike {
|
||||
metadata?: Record<string, unknown> | null;
|
||||
address_1?: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
function destinationDaneCode(address: AddressLike | null | undefined): string | null {
|
||||
const cityId = address?.metadata?.cityId;
|
||||
return typeof cityId === "number" ? toEnvioClickDaneCode(cityId) : null;
|
||||
}
|
||||
|
||||
// Agrega el carrito/orden a UN solo paquete (EnvioClick solo acepta 1 por
|
||||
// cotizacion/guia, confirmado en la doc real: "min 1, max 1"). Si algun
|
||||
// item no tiene peso real cargado (hoy NINGUN producto lo tiene, ver
|
||||
// plan de implementacion), se usa el paquete de fallback conservador
|
||||
// completo en vez de mezclar datos reales con inventados a medias.
|
||||
function aggregatePackage(items: Array<{ quantity: number; variant?: { weight?: number | null } | null }>) {
|
||||
if (items.length === 0) return { ...DEFAULT_PACKAGE };
|
||||
let totalWeight = 0;
|
||||
for (const item of items) {
|
||||
const weight = item.variant?.weight;
|
||||
if (!weight) return { ...DEFAULT_PACKAGE };
|
||||
totalWeight += weight * item.quantity;
|
||||
}
|
||||
return { ...DEFAULT_PACKAGE, weight: totalWeight };
|
||||
}
|
||||
|
||||
export default class EnvioClickFulfillmentService extends AbstractFulfillmentProviderService {
|
||||
static identifier = "envioclick";
|
||||
|
||||
protected logger_: Logger;
|
||||
protected options_: EnvioClickOptions;
|
||||
|
||||
constructor(container: InjectedDependencies, options: EnvioClickOptions) {
|
||||
super();
|
||||
this.logger_ = container.logger;
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
static validateOptions(options: Record<string, unknown>): void {
|
||||
for (const key of ["apiKey", "originDaneCode", "originAddress", "originCompany", "originFirstName", "originLastName", "originEmail", "originPhone"]) {
|
||||
if (!options[key]) {
|
||||
throw new Error(`envioclick-fulfillment: falta la opción "${key}" en medusa-config.ts`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
|
||||
// Una sola opcion: siempre toma la tarifa mas barata entre las
|
||||
// transportadoras que cotizan, mismo criterio que el plugin oficial
|
||||
// de WooCommerce de referencia ("toma el precio mas economico").
|
||||
return [{ id: "standard", name: "Envío nacional (calculado)" }];
|
||||
}
|
||||
|
||||
async validateFulfillmentData(
|
||||
optionData: Record<string, unknown>,
|
||||
data: Record<string, unknown>,
|
||||
_context: ValidateFulfillmentDataContext,
|
||||
): Promise<any> {
|
||||
return data;
|
||||
}
|
||||
|
||||
async validateOption(_data: Record<string, unknown>): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async canCalculate(_data: CreateShippingOptionDTO): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Se invoca en vivo durante el checkout cada vez que se piden/refrescan
|
||||
// las shipping options del carrito. Medusa advierte explicitamente que
|
||||
// un error acá bloquea el checkout entero -- por eso NUNCA se deja
|
||||
// propagar una excepcion: cualquier falla (red, ciudad sin id DIVIPOLA,
|
||||
// EnvioClick caido) cae al precio fijo que ya se usaba antes de esta
|
||||
// integracion, para no romper la compra.
|
||||
async calculatePrice(
|
||||
optionData: CalculateShippingOptionPriceDTO["optionData"],
|
||||
_data: CalculateShippingOptionPriceDTO["data"],
|
||||
context: CalculateShippingOptionPriceContext,
|
||||
): Promise<CalculatedShippingOptionPrice> {
|
||||
const fallback: CalculatedShippingOptionPrice = {
|
||||
calculated_amount: FALLBACK_PRICE_COP,
|
||||
is_calculated_price_tax_inclusive: false,
|
||||
};
|
||||
try {
|
||||
const address = context.shipping_address as AddressLike | undefined;
|
||||
const daneCode = destinationDaneCode(address);
|
||||
if (!daneCode) {
|
||||
this.logger_.warn("envioclick-fulfillment: dirección sin id DIVIPOLA en metadata, usando precio fijo de respaldo");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const items = (context.items ?? []) as Array<{ quantity: number; variant?: { weight?: number | null } | null }>;
|
||||
const pkg = aggregatePackage(items);
|
||||
|
||||
const rates = await quoteShipment(
|
||||
{ apiKey: this.options_.apiKey },
|
||||
{
|
||||
pkg,
|
||||
description: "Pedido ARI Shopping",
|
||||
contentValue: 50000,
|
||||
origin: { daneCode: this.options_.originDaneCode, address: this.options_.originAddress },
|
||||
destination: { daneCode, address: address?.address_1 || "Dirección de entrega" },
|
||||
},
|
||||
);
|
||||
|
||||
if (rates.length === 0) return fallback;
|
||||
|
||||
const cheapest = rates.reduce((min, rate) =>
|
||||
rate.flete + rate.minimumInsurance < min.flete + min.minimumInsurance ? rate : min,
|
||||
);
|
||||
|
||||
return {
|
||||
calculated_amount: cheapest.flete + cheapest.minimumInsurance,
|
||||
is_calculated_price_tax_inclusive: false,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger_.warn(`envioclick-fulfillment: falló calculatePrice (${(error as Error).message}), usando precio fijo de respaldo`);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// A diferencia de calculatePrice, acá SÍ se deja propagar el error --
|
||||
// el idRate de la cotización mostrada en checkout no se persiste en
|
||||
// ningún lado (CalculatedShippingOptionPrice no tiene un campo `data`
|
||||
// para eso, confirmado leyendo el tipo real), así que se vuelve a
|
||||
// cotizar en el momento de generar la guía (más correcto igual: el
|
||||
// fulfillment puede pasar horas/días después del checkout, un idRate
|
||||
// viejo podría estar vencido) y se reserva con la tarifa más barata de
|
||||
// esa cotización fresca.
|
||||
async createFulfillment(
|
||||
_data: Record<string, unknown>,
|
||||
_items: Partial<Omit<FulfillmentItemDTO, "fulfillment">>[],
|
||||
order: Partial<FulfillmentOrderDTO> | undefined,
|
||||
_fulfillment: Partial<Omit<FulfillmentDTO, "provider_id" | "data" | "items">>,
|
||||
): Promise<CreateFulfillmentResult> {
|
||||
const address = order?.shipping_address as AddressLike | undefined;
|
||||
const daneCode = destinationDaneCode(address);
|
||||
if (!daneCode) {
|
||||
throw new Error("envioclick-fulfillment: no se puede generar la guía sin un id DIVIPOLA en la dirección de envío.");
|
||||
}
|
||||
|
||||
const items = (order?.items ?? []) as unknown as Array<{ quantity: number; variant?: { weight?: number | null } | null }>;
|
||||
const pkg = aggregatePackage(items);
|
||||
|
||||
const rates = await quoteShipment(
|
||||
{ apiKey: this.options_.apiKey },
|
||||
{
|
||||
pkg,
|
||||
description: "Pedido ARI Shopping",
|
||||
contentValue: 50000,
|
||||
origin: { daneCode: this.options_.originDaneCode, address: this.options_.originAddress },
|
||||
destination: { daneCode, address: address?.address_1 || "Dirección de entrega" },
|
||||
},
|
||||
);
|
||||
if (rates.length === 0) {
|
||||
throw new Error("envioclick-fulfillment: no hay tarifas disponibles para generar la guía.");
|
||||
}
|
||||
const cheapest = rates.reduce((min, rate) =>
|
||||
rate.flete + rate.minimumInsurance < min.flete + min.minimumInsurance ? rate : min,
|
||||
);
|
||||
|
||||
const guide = await createGuide(
|
||||
{ apiKey: this.options_.apiKey },
|
||||
this.options_.sandbox,
|
||||
{
|
||||
idRate: cheapest.idRate,
|
||||
myShipmentReference: `Orden ${order?.display_id ?? order?.id ?? ""}`,
|
||||
externalOrderId: String(order?.id ?? ""),
|
||||
pkg,
|
||||
description: "Pedido ARI Shopping",
|
||||
contentValue: 50000,
|
||||
origin: {
|
||||
company: this.options_.originCompany,
|
||||
firstName: this.options_.originFirstName,
|
||||
lastName: this.options_.originLastName,
|
||||
email: this.options_.originEmail,
|
||||
phone: this.options_.originPhone,
|
||||
address: this.options_.originAddress,
|
||||
suburb: "N/A",
|
||||
crossStreet: "N/A",
|
||||
reference: "N/A",
|
||||
daneCode: this.options_.originDaneCode,
|
||||
},
|
||||
destination: {
|
||||
company: "N/A",
|
||||
firstName: address?.first_name || "Cliente",
|
||||
lastName: address?.last_name || "ARI",
|
||||
email: address?.email || order?.email || "[email protected]",
|
||||
phone: address?.phone || "0000000000",
|
||||
address: address?.address_1 || "Dirección de entrega",
|
||||
suburb: "N/A",
|
||||
crossStreet: "N/A",
|
||||
reference: "N/A",
|
||||
daneCode,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
idOrder: guide.idOrder,
|
||||
tracker: guide.tracker,
|
||||
labelUrl: guide.url,
|
||||
},
|
||||
labels: [
|
||||
{
|
||||
tracking_number: guide.tracker,
|
||||
tracking_url: guide.url,
|
||||
label_url: guide.url,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async cancelFulfillment(data: Record<string, unknown>): Promise<any> {
|
||||
const idOrder = data.idOrder;
|
||||
if (typeof idOrder !== "number") {
|
||||
this.logger_.warn("envioclick-fulfillment: cancelFulfillment sin idOrder guardado, no hay nada que cancelar en EnvíoClick");
|
||||
return { data };
|
||||
}
|
||||
await cancelGuides({ apiKey: this.options_.apiKey }, [idOrder]);
|
||||
return { data };
|
||||
}
|
||||
|
||||
async getFulfillmentDocuments(_data: Record<string, unknown>): Promise<never[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async createReturnFulfillment(_fulfillment: Record<string, unknown>): Promise<CreateFulfillmentResult> {
|
||||
// Devoluciones no estan en alcance de esta primera version.
|
||||
throw new Error("envioclick-fulfillment: createReturnFulfillment no está implementado todavía.");
|
||||
}
|
||||
|
||||
async getReturnDocuments(_data: Record<string, unknown>): Promise<never[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getShipmentDocuments(_data: Record<string, unknown>): Promise<never[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async retrieveDocuments(_fulfillmentData: Record<string, unknown>, _documentType: string): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export interface EnvioClickOptions {
|
||||
apiKey: string;
|
||||
// El unico endpoint con modo de pruebas real es la generacion de guia
|
||||
// (ver src/lib/envioclick.ts) -- la cotizacion siempre usa tarifas
|
||||
// reales, no hay nada que alternar ahi.
|
||||
sandbox: boolean;
|
||||
// Datos de la bodega de origen -- la stock location real todavia no
|
||||
// tiene direccion fisica cargada ("Sin dirección física registrada"),
|
||||
// asi que estos vienen de config hasta que se cargue una real.
|
||||
originDaneCode: string;
|
||||
originAddress: string;
|
||||
originCompany: string;
|
||||
originFirstName: string;
|
||||
originLastName: string;
|
||||
originEmail: string;
|
||||
originPhone: string;
|
||||
}
|
||||
|
||||
// Precio de fallback cuando EnvíoClick falla o no hay peso/dimensiones
|
||||
// reales del pedido (hoy ningun producto los tiene cargados) -- mismo
|
||||
// valor que ya usaba la shipping option fija de ADR 0009, para no
|
||||
// empeorar la experiencia mientras no haya datos reales.
|
||||
export const FALLBACK_PRICE_COP = 15000;
|
||||
|
||||
// Paquete por defecto cuando algun item del carrito no tiene
|
||||
// peso/dimensiones reales -- conservador, no preciso. Ver nota en el
|
||||
// plan de implementacion: esto es una tarea de datos, no de codigo.
|
||||
export const DEFAULT_PACKAGE = {
|
||||
weight: 1,
|
||||
length: 20,
|
||||
height: 20,
|
||||
width: 20,
|
||||
};
|
||||
@@ -33,3 +33,18 @@ data:
|
||||
EPAYCO_ENV: sandbox
|
||||
EPAYCO_RESPONSE_URL: https://shop.cruzcloud.net/checkout/pse/return
|
||||
EPAYCO_NOTIFY_URL: https://commerce.cruzcloud.net/hooks/payment/epayco_pse
|
||||
|
||||
# EnvíoClick (ADR 0009 §2) -- fulfillment provider real por peso/volumen,
|
||||
# reemplaza la tarifa fija. Solo config no sensible; ENVIOCLICK_API_KEY
|
||||
# va en secrets.template.yaml. ENVIOCLICK_SANDBOX=true usa el endpoint
|
||||
# de guía de pruebas (/shipment_sandbox) -- la cotización siempre usa
|
||||
# tarifas reales, no tiene modo sandbox. Origen = bodega Bogotá
|
||||
# ("Bodega principal ARI Shopping", sin dirección física real cargada
|
||||
# todavía -- ENVIOCLICK_ORIGIN_ADDRESS es un placeholder hasta que la
|
||||
# haya).
|
||||
ENVIOCLICK_SANDBOX: "true"
|
||||
ENVIOCLICK_ORIGIN_DANE_CODE: "11001000"
|
||||
ENVIOCLICK_ORIGIN_ADDRESS: "Calle 1 No 1-1"
|
||||
ENVIOCLICK_ORIGIN_COMPANY: "ARI Shopping"
|
||||
ENVIOCLICK_ORIGIN_EMAIL: "[email protected]"
|
||||
ENVIOCLICK_ORIGIN_PHONE: "3000000000"
|
||||
@@ -65,7 +65,7 @@ spec:
|
||||
memory: 64Mi
|
||||
|
||||
- name: migrations
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.229
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.234
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- node
|
||||
@@ -86,7 +86,7 @@ spec:
|
||||
|
||||
containers:
|
||||
- name: app
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.229
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.234
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
||||
@@ -47,6 +47,10 @@ stringData:
|
||||
EPAYCO_PRIVATE_KEY: CAMBIAR
|
||||
EPAYCO_P_CUST_ID_CLIENTE: CAMBIAR
|
||||
EPAYCO_P_KEY: CAMBIAR
|
||||
|
||||
# EnvíoClick (ADR 0009 §2) -- API key real de la cuenta
|
||||
# (envioclickpro.com.co > API Key / Desarrolladores).
|
||||
ENVIOCLICK_API_KEY: CAMBIAR
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
|
||||
@@ -88,6 +88,12 @@ export function CheckoutClient() {
|
||||
address1: form.address1,
|
||||
city: selectedCiudad.name,
|
||||
province: selectedDepartamento.name,
|
||||
// ids DIVIPOLA reales -- EnvioClick (fulfillment provider) los
|
||||
// necesita para cotizar sin ambiguedad, el nombre solo no
|
||||
// alcanza (hay municipios con el mismo nombre en distintos
|
||||
// departamentos).
|
||||
cityId: selectedCiudad.id,
|
||||
departmentId: selectedDepartamento.id,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
runAsUser: 1001
|
||||
containers:
|
||||
- name: app
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.230
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.235
|
||||
env:
|
||||
- name: HEADLESS_PROVIDER
|
||||
value: medusa
|
||||
|
||||
@@ -46,6 +46,14 @@ export interface ShippingAddressInput {
|
||||
province?: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
// ids DIVIPOLA/DANE reales (lib/data/colombia.ts) -- se guardan en la
|
||||
// metadata de la direccion porque Medusa no tiene un campo propio para
|
||||
// esto. EnvioClick (fulfillment provider real, ver ADR 0009 §2) los
|
||||
// necesita para cotizar/generar guia sin ambiguedad -- el nombre de
|
||||
// ciudad solo no alcanza (hay nombres de municipio repetidos entre
|
||||
// distintos departamentos en Colombia).
|
||||
cityId?: number;
|
||||
departmentId?: number;
|
||||
}
|
||||
|
||||
export interface ShippingOption {
|
||||
@@ -212,6 +220,10 @@ export async function setShippingAddress(
|
||||
province: address.province || undefined,
|
||||
country_code: "co",
|
||||
phone: address.phone,
|
||||
metadata:
|
||||
address.cityId || address.departmentId
|
||||
? { cityId: address.cityId, departmentId: address.departmentId }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user