Proveedores de dropshipping: importar productos con precio por reglas, sincronizar coste y stock, reenviar pedidos y seguimiento
Instalar
genpm add @core/suppliersQué obtienes
- Código en src/lib/suppliers/, 8 archivos. (48 kB)
- Reglas de IA en src/lib/suppliers/AGENTS.md, más archivos de reglas para tu IDE.
- Variables añadidas a .env.example: SUPPLIER_COST_ALERT_PCT.
- Resuelve @core/catalog, @core/contracts, @core/db, @core/email, @core/jobs, @core/media, @core/money, @core/orders, @core/pricing, @core/rich-text por ti.
README
Este paquete no tiene README.
Esto es exactamente lo que lee tu IA cuando trabaja en src/lib/suppliers. No se añade nada más a su contexto.
@core/suppliers — rules for AI agents
Purpose
Dropshipping operations behind a SupplierAdapter contract: import supplier products into @core/catalog as drafts
(price from @core/pricing rules, images copied to @core/media, plain-text description), map variants to supplier
SKUs with cost and delivery days, sync cost and stock (reprice; pause products that would sell at a loss), turn paid
orders into supplier orders (approval by default, or automatic), forward them with retries, record tracking on the
order and keep an exceptions queue. Ships the manual adapter (CSV import, orders emailed to the supplier).
Map
index.ts— public API:upsertSupplier,parseSupplierCsv,importProduct,syncSupplier,setFxRatesProvider,approveSupplierOrder,forwardSupplierOrder,recordSupplierShipment,deliveryEstimate,registerAdapter,suppliersAdminResources.suppliers.ts— flows and jobs (suppliers.forward,suppliers.sync,suppliers.tracking).adapter.ts— contract.adapters/manual.ts— manual adapter and CSV parser.admin.ts— suppliers, supplier orders, exceptions.
Integration
- Install @core/pricing and create at least one price rule. Migrations as in
src/lib/db/AGENTS.md; the @core/jobs cron must run. - Import this module at startup (registers the manual adapter and the
paidorder handler). - Create a supplier in the admin: adapter
manual,config.orderEmail, currency. API adapters read credentials from env, never fromconfig. - Import:
importProduct(supplierId, parseSupplierCsv(csv, currency)[0], { slug, categories })→ review the draft, then publish. - Schedule
syncAllJob(e.g. every 6 h) andpollTrackingJob(every 12 h) with @core/jobsschedule. Suppliers in another currency need exchange rates for the scheduled sync:setFxRatesProvider(async () => ({ base, rates }))at startup, or envSUPPLIER_FX_RATES='{"base":"EUR","rates":{"USD":1.08}}'. Without them, products whose cost changed are paused with anfx_rates_missingexception. A failing supplier gets async_failedexception; the rest still sync. - Product pages: show
deliveryEstimate(variantId)before purchase. - Add
...suppliersAdminResources()tosrc/genpm/admin.ts; check open exceptions daily.
Conventions
- New adapters implement
SupplierAdapterin their own file and callregisterAdapter; list their image hosts. - Supplier orders start in
pending_approvalunless the supplier hasautoForward; failures retry, then open an exception. - Forwarding is at most once: the row is claimed (
queued → sending) beforeplaceOrder. A timeout or a row found insendingmeans the outcome is unknown: it goes tofailedwith aforward_failedexception and is never re-sent automatically — check with the supplier before approving it again. Only non-refunded units are forwarded. - Adapters:
placeOrderthrows only when the order was NOT created, usesorder.referenceas idempotency key when the API allows it, and passesopts.signaltofetch. - Costs may be in another currency: pass exchange
rates(and store them) when importing or syncing.
Don't
- Don't scrape supplier websites or bypass their terms; use official APIs or the manual adapter.
- Don't forward unpaid, cancelled or refunded orders, and don't sell below cost (sync pauses those products).
- Don't hotlink supplier images or copy descriptions claiming features/certifications you can't verify.
# @core/suppliers — rules for AI agents
## Purpose
Dropshipping operations behind a `SupplierAdapter` contract: import supplier products into @core/catalog as drafts
(price from @core/pricing rules, images copied to @core/media, plain-text description), map variants to supplier
SKUs with cost and delivery days, sync cost and stock (reprice; pause products that would sell at a loss), turn paid
orders into supplier orders (approval by default, or automatic), forward them with retries, record tracking on the
order and keep an exceptions queue. Ships the `manual` adapter (CSV import, orders emailed to the supplier).
## Map
- `index.ts` — public API: `upsertSupplier`, `parseSupplierCsv`, `importProduct`, `syncSupplier`, `setFxRatesProvider`, `approveSupplierOrder`, `forwardSupplierOrder`, `recordSupplierShipment`, `deliveryEstimate`, `registerAdapter`, `suppliersAdminResources`.
- `suppliers.ts` — flows and jobs (`suppliers.forward`, `suppliers.sync`, `suppliers.tracking`). `adapter.ts` — contract.
- `adapters/manual.ts` — manual adapter and CSV parser. `admin.ts` — suppliers, supplier orders, exceptions.
## Integration
1. Install @core/pricing and create at least one price rule. Migrations as in `src/lib/db/AGENTS.md`; the @core/jobs cron must run.
2. Import this module at startup (registers the manual adapter and the `paid` order handler).
3. Create a supplier in the admin: adapter `manual`, `config.orderEmail`, currency. API adapters read credentials from env, never from `config`.
4. Import: `importProduct(supplierId, parseSupplierCsv(csv, currency)[0], { slug, categories })` → review the draft, then publish.
5. Schedule `syncAllJob` (e.g. every 6 h) and `pollTrackingJob` (every 12 h) with @core/jobs `schedule`. Suppliers
in another currency need exchange rates for the scheduled sync: `setFxRatesProvider(async () => ({ base, rates }))`
at startup, or env `SUPPLIER_FX_RATES='{"base":"EUR","rates":{"USD":1.08}}'`. Without them, products whose cost
changed are paused with an `fx_rates_missing` exception. A failing supplier gets a `sync_failed` exception; the rest still sync.
6. Product pages: show `deliveryEstimate(variantId)` before purchase.
7. Add `...suppliersAdminResources()` to `src/genpm/admin.ts`; check open exceptions daily.
## Conventions
- New adapters implement `SupplierAdapter` in their own file and call `registerAdapter`; list their image hosts.
- Supplier orders start in `pending_approval` unless the supplier has `autoForward`; failures retry, then open an exception.
- Forwarding is at most once: the row is claimed (`queued → sending`) before `placeOrder`. A timeout or a row found
in `sending` means the outcome is unknown: it goes to `failed` with a `forward_failed` exception and is never
re-sent automatically — check with the supplier before approving it again. Only non-refunded units are forwarded.
- Adapters: `placeOrder` throws only when the order was NOT created, uses `order.reference` as idempotency key when
the API allows it, and passes `opts.signal` to `fetch`.
- Costs may be in another currency: pass exchange `rates` (and store them) when importing or syncing.
## Don't
- Don't scrape supplier websites or bypass their terms; use official APIs or the manual adapter.
- Don't forward unpaid, cancelled or refunded orders, and don't sell below cost (sync pauses those products).
- Don't hotlink supplier images or copy descriptions claiming features/certifications you can't verify.
El árbol exacto que se inyectará, tras aplicar .genpmignore. Anclado a
// Adaptador manual: para cualquier proveedor sin API. Productos por CSV, pedidos por email al proveedor y seguimiento
// introducido en el panel. Configuración: `orderEmail` (a quién se envían los pedidos).
import { getEmailProvider } from '../../email/index.ts';
import { parseMoney } from '../../money/index.ts';
import { registerAdapter, type ExternalProduct, type SupplierAdapter } from '../adapter.ts';
/**
* CSV con cabecera: `product_id,title,description,image_urls,option1_name,option1_value,option2_name,option2_value,variant_id,cost,stock,weight_grams,ship_min_days,ship_max_days`
* (una fila por variante; `image_urls` separadas por `|`; `cost` decimal en la moneda del proveedor).
*/
export function parseSupplierCsv(csv: string, currency: string): ExternalProduct[] {
const rows = parseCsv(csv);
const [header, ...data] = rows;
if (!header) return [];
const col = (name: string) => header.indexOf(name);
for (const required of ['product_id', 'title', 'variant_id', 'cost']) if (col(required) < 0) throw new Error(`CSV is missing column ${required}`);
const get = (r: string[], name: string) => (col(name) >= 0 ? (r[col(name)] ?? '').trim() : '');
const byProduct = new Map<string, ExternalProduct>();
for (const r of data) {
if (!r.some((c) => c.trim())) continue;
const pid = get(r, 'product_id');
let p = byProduct.get(pid);
if (!p) {
const min = Number(get(r, 'ship_min_days'));
const max = Number(get(r, 'ship_max_days'));
p = {
externalProductId: pid,
title: get(r, 'title').slice(0, 200),
description: get(r, 'description').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 10_000),
images: get(r, 'image_urls').split('|').map((u) => u.trim()).filter((u) => u.startsWith('https://')).slice(0, 10),
options: [],
variants: [],
shippingDays: min && max ? { min, max } : null,
};
byProduct.set(pid, p);
}
const options: Record<string, string> = {};
for (const i of [1, 2, 3]) {
const name = get(r, `option${i}_name`);
const value = get(r, `option${i}_value`);
if (!name || !value) continue;
options[name] = value;
const opt = p.options.find((o) => o.name === name) ?? (p.options.push({ name, values: [] }), p.options.at(-1)!);
if (!opt.values.includes(value)) opt.values.push(value);
}
const stock = get(r, 'stock');
const weight = get(r, 'weight_grams');
p.variants.push({ externalVariantId: get(r, 'variant_id'), options, cost: parseMoney(get(r, 'cost'), currency), stock: stock === '' ? null : Number(stock), weightGrams: weight === '' ? null : Number(weight) });
}
return [...byProduct.values()];
}
/** CSV mínimo (RFC 4180): comillas dobles, comas y saltos de línea dentro de comillas. */
export function parseCsv(text: string): string[][] {
const rows: string[][] = [];
let row: string[] = [];
let cell = '';
let quoted = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i]!;
if (quoted) {
if (ch === '"' && text[i + 1] === '"') {
cell += '"';
i++;
} else if (ch === '"') quoted = false;
else cell += ch;
} else if (ch === '"') quoted = true;
else if (ch === ',') {
row.push(cell);
cell = '';
} else if (ch === '\n' || ch === '\r') {
if (ch === '\r' && text[i + 1] === '\n') i++;
row.push(cell);
rows.push(row);
row = [];
cell = '';
} else cell += ch;
}
if (cell || row.length) {
row.push(cell);
rows.push(row);
}
return rows;
}
const esc = (s: string) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
export const manualAdapter: SupplierAdapter = {
name: 'manual',
imageHosts: [],
async placeOrder(supplier, order) {
const to = supplier.config.orderEmail;
const from = process.env.EMAIL_FROM;
if (!to || !from) throw new Error('manual supplier needs config.orderEmail and EMAIL_FROM');
const a = order.address;
const lines = order.lines.map((l) => `${l.externalVariantId} × ${l.quantity}`);
const address = [a.name, a.company, a.line1, a.line2, `${a.postalCode} ${a.city}`, a.region, a.country, a.phone].filter(Boolean) as string[];
await getEmailProvider().send({
from,
to,
subject: `New order ${order.reference}`,
text: [`Order: ${order.reference}`, '', ...lines, '', 'Ship to:', ...address].join('\n'),
html: `<p>Order: <strong>${esc(order.reference)}</strong></p><ul>${lines.map((l) => `<li>${esc(l)}</li>`).join('')}</ul><p>Ship to:<br>${address.map(esc).join('<br>')}</p>`,
});
return { externalRef: order.reference };
},
};
registerAdapter(manualAdapter);
Este paquete no declara servidores MCP.
| Versión | Commit | Publicado | Escaneo |
|---|---|---|---|
| 1.1.0 | 4e946f2 | hace 3 horas | escaneo superado |
- genpm
- @core/catalog ^1.0.0@core/contracts ^1.0.0@core/db ^1.0.0@core/email ^1.0.1@core/jobs ^1.0.0@core/media ^1.0.0@core/money ^1.0.0@core/orders ^1.0.0@core/pricing ^1.0.0@core/rich-text ^1.0.0
- npm
- zod ^4.0.0
- propuesta
- GenPM propone el comando npm y solo lo ejecuta si dices que sí.
- Usado por (1)
- @core/kit-dropshipping ^1.0.0
- escaneo
- escaneo superado · 0 hallazgos
- commit
- v1.1.0 → 4e946f226d5dd3af9dbeb2209701da45a13e8769 · verificado tras la descarga
- scripts
- Ninguno. GenPM nunca ejecuta código del paquete.
- licencia
- MIT
- Calidad
- 100/100
- Licencia reconocidacumplido
- AGENTS.md explica su propósitocumplido
- AGENTS.md tiene pasos de integracióncumplido
- AGENTS.md lista convenciones o prohibicionescumplido
- Incluye testscumplido
- Escaneo de seguridad superadocumplido
- Publicado en los últimos 6 mesescumplido
- Publicador verificadocumplido
- Resumen y palabras clavecumplido
- reporte
- ¿Ves algo raro?