From c05c0b05823346865a236fb0cf2cb0182af3c897 Mon Sep 17 00:00:00 2001 From: chattie Date: Fri, 21 Aug 2026 12:27:13 +0200 Subject: [PATCH] feat(F-113): completed feature --- backlog/features.json | 75 ++++++++++++++++++ .../src/app/(dashboard)/orders/[id]/page.tsx | 71 +++++++++++++++-- .../src/app/(dashboard)/settings/page.tsx | 28 +++++++ project/apps/admin/src/lib/api-client.ts | 19 +++-- project/apps/admin/src/types/index.ts | 1 + project/apps/admin/tsconfig.tsbuildinfo | 2 +- project/migrations/039_order_courier.js | 12 +++ .../src/modules/orders/api/orders.routes.ts | 29 ++++--- .../orders/application/order-service.ts | 4 +- project/src/modules/orders/domain/order.ts | 1 + project/src/modules/orders/domain/ports.ts | 4 +- .../infrastructure/order-status-mailer.ts | 26 ++++++- .../infrastructure/pg-order-repository.ts | 8 +- .../orders/tests/order-service.test.ts | 17 +++++ .../orders/tests/order-status-mailer.test.ts | 55 ++++++++++++++ .../store-settings/api/settings.routes.ts | 29 +++++++ .../tests/settings-couriers.test.ts | 29 +++++++ work/artifacts/F-113/architect.md | 40 ++++++++++ work/artifacts/F-113/implementer.md | 44 +++++++++++ work/artifacts/F-113/leader-close.json | 15 ++++ work/artifacts/F-113/qa.json | 21 +++++ work/artifacts/F-113/reviewer.json | 19 +++++ work/artifacts/F-113/security.json | 17 +++++ work/current.md | 13 +++- work/runtime-status.json | 76 +++++++++---------- 25 files changed, 581 insertions(+), 74 deletions(-) create mode 100644 project/migrations/039_order_courier.js create mode 100644 project/src/modules/orders/tests/order-status-mailer.test.ts create mode 100644 project/src/modules/store-settings/tests/settings-couriers.test.ts create mode 100644 work/artifacts/F-113/architect.md create mode 100644 work/artifacts/F-113/implementer.md create mode 100644 work/artifacts/F-113/leader-close.json create mode 100644 work/artifacts/F-113/qa.json create mode 100644 work/artifacts/F-113/reviewer.json create mode 100644 work/artifacts/F-113/security.json diff --git a/backlog/features.json b/backlog/features.json index 9ef59d0..7c91510 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -4862,6 +4862,81 @@ "security": false, "qa": false } + }, + { + "id": "F-113", + "type": "feature", + "title": "Email customer on processing/shipped with tracking and editable courier", + "problem": "Customer gets no email when order moves to processing or shipped; shipped email does not say which courier carries the parcel", + "goal": "Send email on PROCESSING and SHIPPED transitions; shipped email includes tracking number and courier chosen from an admin-editable courier list", + "scope_in": [ + "order status mailer", + "admin transition flow", + "courier list settings", + "order courier field" + ], + "scope_out": [ + "no redesign of other state emails", + "no external carrier API integration" + ], + "priority": "high", + "risk": "med", + "description": "Problem: Customer gets no email when order moves to processing or shipped; shipped email does not say which courier carries the parcel. Goal: Send email on PROCESSING and SHIPPED transitions; shipped email includes tracking number and courier chosen from an admin-editable courier list. Scope IN: order status mailer, admin transition flow, courier list settings, order courier field. Scope OUT: no redesign of other state emails, no external carrier API integration. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "PROCESSING transition sends email to customer when SMTP configured", + "SHIPPED transition email shows tracking number and courier", + "Courier comes from admin-editable list stored in settings", + "Selecting courier is required when marking order shipped", + "Missing or failing SMTP reports notified false without breaking transition", + "Typecheck, tests, verify pass" + ], + "status": "done", + "created_at": "2026-08-21", + "gates": { + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-21T10:27:13Z" + }, + { + "id": "F-114", + "type": "feature", + "title": "Import legacy OpenCart categories into the catalog without duplicates", + "problem": "Legacy OpenCart database has 76 categories including product categories and brand names; current catalog only has 5 root categories and is missing many product subcategories and brand entries, blocking migration of the old inventory", + "goal": "Map and import the legacy oc_category_description entries into the active categories module, normalizing HTML entities and skipping exact-name duplicates; route brand-looking entries into the existing brand module instead of categories", + "scope_in": [ + "categories module", + "brands module", + "mapping helper", + "legacy category normalization", + "admin seed script" + ], + "scope_out": [ + "no product migration", + "no design changes", + "no category attribute redesign" + ], + "priority": "high", + "risk": "med", + "description": "Problem: Legacy OpenCart database has 76 categories including product categories and brand names; current catalog only has 5 root categories and is missing many product subcategories and brand entries, blocking migration of the old inventory. Goal: Map and import the legacy oc_category_description entries into the active categories module, normalizing HTML entities and skipping exact-name duplicates; route brand-looking entries into the existing brand module instead of categories. Scope IN: categories module, brands module, mapping helper, legacy category normalization, admin seed script. Scope OUT: no product migration, no design changes, no category attribute redesign. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Legacy oc_category_description entries are parsed and normalized (HTML entities and casing)", + "Existing category names are skipped to avoid duplicates (case-insensitive match)", + "Brand-looking entries (e.g. SOLGAR, EL GRANERO INTEGRAL, BIOSPIRIT) are created in the brands module, not categories", + "New product categories are added as subcategories under the existing root (Alimentacion, Cosmetica e Higiene, Hogar y Mascotas, Limpieza Ecologica, Suplementos) or as new roots when they do not fit", + "Each new category has a unique slug and a sensible parent", + "Seed script is idempotent and safe to run multiple times", + "Typecheck, tests, verify pass" + ], + "status": "pending", + "created_at": "2026-08-21", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } } ] } diff --git a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx index ff57c40..b6c3021 100644 --- a/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx +++ b/project/apps/admin/src/app/(dashboard)/orders/[id]/page.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams } from 'next/navigation'; import Link from 'next/link'; import type { Order, OrderState, Product, OrderHistoryEvent } from '@/types'; -import { ordersApi, productsApi } from '@/lib/api-client'; +import { ordersApi, productsApi, settingsApi } from '@/lib/api-client'; const STATE_LABELS: Record = { PENDING: 'Pendiente', @@ -74,6 +74,8 @@ export default function OrderDetailPage() { const [showConfirm, setShowConfirm] = useState(null); const [confirmReason, setConfirmReason] = useState(''); const [trackingInput, setTrackingInput] = useState(''); + const [courierInput, setCourierInput] = useState(''); + const [couriers, setCouriers] = useState([]); const [notification, setNotification] = useState<{ ok: boolean; message: string } | null>(null); // Edición de artículos @@ -90,6 +92,7 @@ export default function OrderDetailPage() { // Envío e historial (F-111) const [history, setHistory] = useState([]); const [shippingTracking, setShippingTracking] = useState(''); + const [shippingCourier, setShippingCourier] = useState(''); const [shippingNote, setShippingNote] = useState(''); const [savingShipping, setSavingShipping] = useState(false); const [shippingMsg, setShippingMsg] = useState<{ ok: boolean; text: string } | null>(null); @@ -110,6 +113,7 @@ export default function OrderDetailPage() { const data = await ordersApi.get(id); setOrder(data); setShippingTracking(data.trackingNumber ?? ''); + setShippingCourier(data.courier ?? ''); } catch (err) { setError(err instanceof Error ? err.message : 'Error al cargar'); } finally { @@ -119,16 +123,30 @@ export default function OrderDetailPage() { useEffect(() => { load(); loadHistory(); }, [load, loadHistory]); + useEffect(() => { + settingsApi + .get() + .then((settings) => setCouriers(settings.couriers ?? [])) + .catch(() => setCouriers([])); + }, []); + const handleTransition = async (nextState: OrderState) => { - if (nextState === 'SHIPPED' && !trackingInput.trim()) return; + if (nextState === 'SHIPPED' && (!trackingInput.trim() || !courierInput.trim())) return; setTransitioning(true); try { - const updated = await ordersApi.transition(id, nextState, trackingInput); + const updated = await ordersApi.transition( + id, + nextState, + nextState === 'SHIPPED' ? trackingInput : undefined, + nextState === 'SHIPPED' ? courierInput : undefined, + ); setOrder(updated); setShippingTracking(updated.trackingNumber ?? ''); + setShippingCourier(updated.courier ?? ''); setShowConfirm(null); setConfirmReason(''); setTrackingInput(''); + setCourierInput(''); loadHistory(); if (updated.notified) { setNotification({ ok: true, message: 'Cliente notificado por email.' }); @@ -256,9 +274,10 @@ export default function OrderDetailPage() { setSavingShipping(true); setShippingMsg(null); try { - const updated = await ordersApi.updateShipping(id, tracking, shippingNote); + const updated = await ordersApi.updateShipping(id, tracking, shippingNote, shippingCourier); setOrder(updated); setShippingTracking(updated.trackingNumber ?? tracking); + setShippingCourier(updated.courier ?? ''); setShippingNote(''); setShippingMsg({ ok: true, text: 'Envío actualizado y registrado en el historial' }); loadHistory(); @@ -342,7 +361,11 @@ export default function OrderDetailPage() { {allowed.map((next) => (