Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e42d588514 | ||
|
|
e2938248f8 |
No files matched your search
@@ -33,16 +33,7 @@ ENV CI=true \
|
|||||||
EPAYCO_P_KEY=build-only-epayco-pkey \
|
EPAYCO_P_KEY=build-only-epayco-pkey \
|
||||||
EPAYCO_ENV=sandbox \
|
EPAYCO_ENV=sandbox \
|
||||||
EPAYCO_RESPONSE_URL=http://localhost:8000/checkout/pse/return \
|
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 ./
|
COPY package.json package-lock.json .npmrc ./
|
||||||
|
|
||||||
|
|||||||
@@ -133,45 +133,6 @@ module.exports = defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
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",
|
resolve: "@medusajs/medusa/file",
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
// 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`;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { ModuleProvider, Modules } from "@medusajs/framework/utils";
|
|
||||||
import EnvioClickFulfillmentService from "./service";
|
|
||||||
|
|
||||||
export default ModuleProvider(Modules.FULFILLMENT, {
|
|
||||||
services: [EnvioClickFulfillmentService],
|
|
||||||
});
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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,18 +33,3 @@ data:
|
|||||||
EPAYCO_ENV: sandbox
|
EPAYCO_ENV: sandbox
|
||||||
EPAYCO_RESPONSE_URL: https://shop.cruzcloud.net/checkout/pse/return
|
EPAYCO_RESPONSE_URL: https://shop.cruzcloud.net/checkout/pse/return
|
||||||
EPAYCO_NOTIFY_URL: https://commerce.cruzcloud.net/hooks/payment/epayco_pse
|
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
|
memory: 64Mi
|
||||||
|
|
||||||
- name: migrations
|
- name: migrations
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.229
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.233
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- node
|
- node
|
||||||
@@ -86,7 +86,7 @@ spec:
|
|||||||
|
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.229
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.233
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ stringData:
|
|||||||
EPAYCO_PRIVATE_KEY: CAMBIAR
|
EPAYCO_PRIVATE_KEY: CAMBIAR
|
||||||
EPAYCO_P_CUST_ID_CLIENTE: CAMBIAR
|
EPAYCO_P_CUST_ID_CLIENTE: CAMBIAR
|
||||||
EPAYCO_P_KEY: 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
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
|
|||||||
@@ -88,12 +88,6 @@ export function CheckoutClient() {
|
|||||||
address1: form.address1,
|
address1: form.address1,
|
||||||
city: selectedCiudad.name,
|
city: selectedCiudad.name,
|
||||||
province: selectedDepartamento.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,
|
phone: form.phone,
|
||||||
email: form.email,
|
email: form.email,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.230
|
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.232
|
||||||
env:
|
env:
|
||||||
- name: HEADLESS_PROVIDER
|
- name: HEADLESS_PROVIDER
|
||||||
value: medusa
|
value: medusa
|
||||||
|
|||||||
@@ -46,14 +46,6 @@ export interface ShippingAddressInput {
|
|||||||
province?: string;
|
province?: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
email: 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 {
|
export interface ShippingOption {
|
||||||
@@ -220,10 +212,6 @@ export async function setShippingAddress(
|
|||||||
province: address.province || undefined,
|
province: address.province || undefined,
|
||||||
country_code: "co",
|
country_code: "co",
|
||||||
phone: address.phone,
|
phone: address.phone,
|
||||||
metadata:
|
|
||||||
address.cityId || address.departmentId
|
|
||||||
? { cityId: address.cityId, departmentId: address.departmentId }
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user