Skip to content

Maxx ERP Blueprint

This document defines the full equivalent of what AncoraERP does for maxxcomputers, rebuilt lean in SvelteKit + PocketBase + TypeScript. Nothing gets lost. Everything gets better.


graph TD
    subgraph Frontend["SvelteKit — Cloudflare Pages"]
        UI["~20 routes\n(SSR + client navigation)"]
    end

    subgraph Backend["PocketBase — self-hosted / Fly.io"]
        COLL["Collections + REST + Realtime"]
        HOOKS["JS Hooks\n(onRecordBefore/AfterCreate)\n— all business logic lives here"]
        SCHED["Scheduled jobs\n(BNR rates, supplier feeds)"]
        FILES["File storage\n(invoice PDFs, attachments)"]
    end

    subgraph External["External APIs"]
        BNR["bnr.ro XML\ncurrency rates"]
        ANAF["webservicesp.anaf.ro\nCUI lookup + e-Factura SPV"]
        SUPPLIERS["20+ IT distributor\nprice feed APIs"]
        SMTP["Resend\n(transactional email)"]
        COURIER["Innight courier\nXML shipment API"]
    end

    UI <-->|PocketBase SDK| COLL
    HOOKS -->|auto-triggered on write| COLL
    SCHED -->|cron| BNR
    SCHED -->|cron| SUPPLIERS
    UI -->|server-side fetch| ANAF
    HOOKS -->|on invoice approve| SMTP
    UI -->|shipment create| COURIER

erDiagram
    companies {
        string  id          PK
        string  name
        string  cui         "Romanian fiscal code"
        string  reg_com     "Trade register number"
        string  address
        string  iban
        string  bank
        int     county_id   FK
        string  logo        "file"
        string  invoice_series "e.g. MX"
        int     next_invoice_number "auto-incremented in hook"
    }

    users {
        string  id          PK
        string  company     FK
        string  email
        string  name
        string  role        "admin|operator|viewer"
        string  default_warehouse FK
    }

    products {
        string  id          PK
        string  company     FK
        string  cod         "internal code"
        string  name
        string  unit        "BUC|KG|L|M|SET"
        float   vat_rate    "19|9|5|0"
        float   list_price  "sell price lei"
        float   cost_price  "last purchase price"
        float   stock       "computed — updated by hooks"
        string  category    FK
        string  barcode
        bool    track_serial "true for laptops, phones, etc."
        bool    active
    }

    product_supplier_codes {
        string  id          PK
        string  product     FK
        string  supplier    FK "tert"
        string  supplier_code "supplier's own SKU"
        float   supplier_price "latest from price feed"
        date    price_updated_at
    }

    terti {
        string  id          PK
        string  company     FK
        string  name
        string  cui         "fiscal code — validated via ANAF on create"
        string  reg_com
        string  type        "C|F|CF  (client|supplier|both)"
        string  address
        string  county      FK
        string  phone
        string  email
        string  iban
        string  bank
        bool    vat_payer
        bool    active
    }

    warehouses {
        string  id          PK
        string  company     FK
        string  name        "DEPOZIT|MAGAZIN|SERVICE"
        string  type        "storage|retail|service"
    }

    categories {
        string  id          PK
        string  company     FK
        string  name        "Laptops|Telefoane|Periferice|Software|Servicii"
        string  parent      FK "self-reference for tree"
    }

    currencies {
        string  id          PK
        string  company     FK
        date    date
        float   eur
        float   usd
        float   gbp
        float   chf
        string  source      "BNR|manual"
    }
erDiagram
    invoices_out {
        string  id            PK
        string  company       FK
        string  number        "MX-2026-0001 — generated in hook"
        date    date
        string  client        FK "terti"
        string  status        "draft|confirmed|cancelled"
        string  currency      "RON|EUR|USD"
        float   exchange_rate "from currencies on date"
        float   total_net
        float   total_vat
        float   total
        string  payment_type  "cash|bank_transfer|card|check"
        date    due_date
        bool    efactura_sent
        string  efactura_id   "ANAF upload ID"
        string  efactura_status "pending|accepted|rejected"
        string  pdf           "file — generated on confirm"
        string  notes
    }

    invoice_lines_out {
        string  id          PK
        string  invoice     FK "invoices_out"
        string  product     FK
        string  product_name "snapshot at time of invoice"
        float   qty
        string  unit
        float   price       "unit price excl VAT"
        float   vat_rate
        float   discount_pct
        float   line_total  "computed: qty * price * (1-disc)"
        float   line_vat    "computed: line_total * vat_rate"
        string  serial_numbers "JSON array — if track_serial"
    }

    invoices_in {
        string  id          PK
        string  company     FK
        string  supplier    FK "terti"
        string  supplier_invoice_number
        date    date
        date    due_date
        string  status      "draft|received|paid"
        float   total_net
        float   total_vat
        float   total
        string  currency
        float   exchange_rate
        string  notes
        string  attachment  "file — scanned invoice"
    }

    invoice_lines_in {
        string  id          PK
        string  invoice     FK "invoices_in"
        string  product     FK
        string  product_name "snapshot"
        float   qty
        float   price
        float   vat_rate
        float   line_total
        string  serial_numbers "JSON array"
    }

    orders_out {
        string  id          PK
        string  company     FK
        string  number      "CMD-2026-0001"
        date    date
        string  client      FK "terti"
        string  status      "draft|confirmed|fulfilled|cancelled"
        string  agent       FK "users"
        float   total_net
        float   total_vat
        string  notes
    }

    order_lines_out {
        string  id          PK
        string  order       FK "orders_out"
        string  product     FK
        float   qty
        float   price
        float   vat_rate
        float   qty_delivered "updated as invoices are created"
    }

    orders_in {
        string  id          PK
        string  company     FK
        string  number      "PO-2026-0001"
        date    date
        string  supplier    FK "terti"
        string  status      "draft|sent|received|cancelled"
        float   total_net
        string  notes
    }

    order_lines_in {
        string  id          PK
        string  order       FK "orders_in"
        string  product     FK
        float   qty
        float   price
        float   qty_received "updated as supplier invoices arrive"
    }

    stock_movements {
        string  id          PK
        string  company     FK
        string  product     FK
        string  warehouse   FK
        float   qty_delta   "positive = in, negative = out"
        float   unit_value  "cost at time of movement"
        string  doc_type    "invoice_out|invoice_in|transfer|adjustment"
        string  doc_id      "FK to source document"
        date    date
        string  serial_number "if track_serial — one record per unit"
    }

    serial_numbers {
        string  id          PK
        string  company     FK
        string  product     FK
        string  sn          "the serial number"
        string  status      "in_stock|sold|returned|in_service"
        string  client      FK "set when sold"
        date    sold_date
        date    warranty_expires "sold_date + warranty months from product"
        string  invoice_out FK "which invoice it was sold on"
        string  invoice_in  FK "which invoice it was received on"
    }

    warranties {
        string  id          PK
        string  company     FK
        string  serial_number FK
        string  client      FK
        date    received_date
        string  problem_description
        string  status      "open|in_service|returned_ok|returned_replaced|rejected"
        date    closed_date
        string  resolution
        string  return_invoice FK "invoices_out — replacement/repaired"
    }

    payments {
        string  id          PK
        string  company     FK
        string  tert        FK "terti"
        string  direction   "in|out"
        float   amount
        string  currency
        float   exchange_rate
        string  type        "cash|bank_transfer|card|check|promissory"
        date    date
        string  reference   "check number / bank ref / receipt number"
        string  notes
    }

    payment_invoice_links {
        string  id          PK
        string  payment     FK
        string  invoice     FK "invoices_out OR invoices_in"
        string  invoice_type "out|in"
        float   amount_applied
    }

    price_lists {
        string  id          PK
        string  company     FK
        string  name        "Retail|Wholesale|Partner"
        float   discount_pct "applied on top of list_price"
        string  client_category FK
    }

    supplier_offers {
        string  id            PK
        string  company       FK
        string  supplier      FK "terti"
        string  product_code  FK "product_supplier_codes"
        float   price
        int     stock_qty     "supplier's reported stock"
        date    valid_date
        string  currency
    }

    licenses {
        string  id          PK
        string  company     FK
        string  product     FK "software product"
        string  client      FK "terti"
        string  license_key
        date    issue_date
        date    expiry_date "null = perpetual"
        string  invoice_out FK
        string  status      "active|expired|revoked"
        string  notes
    }

    shipments {
        string  id          PK
        string  company     FK
        string  invoice_out FK
        string  courier     "innight|cargus|sameday"
        string  awb
        string  status      "created|picked_up|in_transit|delivered|returned"
        date    created_date
        date    delivered_date
        string  recipient_name
        string  recipient_address
        float   weight_kg
        float   declared_value
    }

All business logic lives in pb_hooks/. No logic in the frontend. The frontend is purely display + forms.

pb_hooks/
├── invoices_out.pb.js — numbering, stock deduction, PDF trigger, e-Factura
├── invoices_in.pb.js — stock increase, cost price update
├── orders_out.pb.js — numbering, stock reservation check
├── orders_in.pb.js — numbering
├── payments.pb.js — mark invoices as paid, cash register balance
├── serial_numbers.pb.js — warranty expiry computation
├── warranties.pb.js — status notifications
├── products.pb.js — low stock alert
├── cron_bnr.pb.js — daily BNR rate fetch at 23:00
└── cron_suppliers.pb.js — nightly supplier price feed imports
pb_hooks/invoices_out.pb.js
// BEFORE CREATE — generate document number atomically
onRecordBeforeCreateRequest((e) => {
const company = $app.dao().findRecordById('companies', e.record.get('company'));
const next = company.getInt('next_invoice_number') + 1;
const series = company.getString('invoice_series');
const year = new Date().getFullYear();
const num = String(next).padStart(4, '0');
e.record.set('number', `${series}-${year}-${num}`);
e.record.set('status', 'draft');
// save incremented counter — serialized, no race condition possible in SQLite
company.set('next_invoice_number', next);
$app.dao().saveRecord(company);
}, 'invoices_out');
// AFTER CREATE status change to "confirmed" — deduct stock + update serial status
onRecordAfterUpdateRequest((e) => {
if (e.record.get('status') !== 'confirmed') return;
if (e.record.getOriginal().get('status') === 'confirmed') return; // already processed
const lines = $app
.dao()
.findRecordsByFilter('invoice_lines_out', 'invoice = {:id}', { id: e.record.id }, -1, 0, []);
for (const line of lines) {
const productId = line.get('product');
const qty = line.getFloat('qty');
const warehouse = e.record.get('warehouse'); // default warehouse from company settings
// 1. Deduct product.stock
const product = $app.dao().findRecordById('products', productId);
const newStock = product.getFloat('stock') - qty;
if (newStock < 0) throw new Error(`Insufficient stock for ${product.getString('name')}`);
product.set('stock', newStock);
$app.dao().saveRecord(product);
// 2. Create stock movement audit record
const movement = new Record($app.dao().findCollectionByNameOrId('stock_movements'));
movement.set('company', e.record.get('company'));
movement.set('product', productId);
movement.set('warehouse', warehouse);
movement.set('qty_delta', -qty);
movement.set('unit_value', line.getFloat('price'));
movement.set('doc_type', 'invoice_out');
movement.set('doc_id', e.record.id);
movement.set('date', e.record.get('date'));
$app.dao().saveRecord(movement);
// 3. Mark serial numbers as sold (if tracked)
const sns = JSON.parse(line.getString('serial_numbers') || '[]');
for (const sn of sns) {
const snRecord = $app
.dao()
.findFirstRecordByFilter('serial_numbers', 'sn = {:sn} && company = {:c}', {
sn,
c: e.record.get('company')
});
snRecord.set('status', 'sold');
snRecord.set('client', e.record.get('client'));
snRecord.set('sold_date', e.record.get('date'));
snRecord.set('invoice_out', e.record.id);
// set warranty expiry
const product2 = $app.dao().findRecordById('products', snRecord.get('product'));
const warrantyMonths = product2.getInt('warranty_months') || 24;
const soldDate = new Date(e.record.get('date'));
soldDate.setMonth(soldDate.getMonth() + warrantyMonths);
snRecord.set('warranty_expires', soldDate.toISOString().split('T')[0]);
$app.dao().saveRecord(snRecord);
}
}
// 4. Queue PDF generation + e-Factura (async via job queue or direct call)
$app.dao().runInTransaction(() => {
schedulePdfGeneration(e.record.id);
if (e.record.get('efactura_required')) {
scheduleEFacturaSubmit(e.record.id);
}
});
}, 'invoices_out');
// AFTER CREATE status change to "cancelled" — reverse stock
onRecordAfterUpdateRequest((e) => {
if (e.record.get('status') !== 'cancelled') return;
if (e.record.getOriginal().get('status') !== 'confirmed') return;
// ... reverse of the above: add stock back, set SNs to in_stock
}, 'invoices_out');
// When supplier invoice is confirmed → increase stock + update cost price
onRecordAfterUpdateRequest((e) => {
if (e.record.get('status') !== 'received') return;
const lines = $app
.dao()
.findRecordsByFilter('invoice_lines_in', 'invoice = {:id}', { id: e.record.id }, -1, 0, []);
for (const line of lines) {
const product = $app.dao().findRecordById('products', line.get('product'));
const incomingQty = line.getFloat('qty');
const incomingPrice = line.getFloat('price');
// weighted average cost price
const currentStock = product.getFloat('stock');
const currentCost = product.getFloat('cost_price');
const newCost =
currentStock === 0
? incomingPrice
: (currentStock * currentCost + incomingQty * incomingPrice) / (currentStock + incomingQty);
product.set('stock', currentStock + incomingQty);
product.set('cost_price', newCost);
$app.dao().saveRecord(product);
// create stock movement
// register serial numbers as in_stock if tracked
}
}, 'invoices_in');
// runs every day at 23:00
cronAdd('bnr_rates', '0 23 * * *', () => {
const response = $http.send({ url: 'https://www.bnr.ro/nbrfxrates.xml', method: 'GET' });
const xml = response.raw;
// parse XML — PocketBase JS has no DOMParser but can regex/split BNR's simple format
const date = xml.match(/PublishingDate>([^<]+)/)[1];
const rates = { eur: 0, usd: 0, gbp: 0, chf: 0 };
for (const [currency, field] of [
['EUR', 'eur'],
['USD', 'usd'],
['GBP', 'gbp'],
['CHF', 'chf']
]) {
const match = xml.match(new RegExp(`currency="${currency}"[^>]*>([0-9.]+)`));
if (match) rates[field] = parseFloat(match[1]);
}
// upsert one record per company
const companies = $app.dao().findRecordsByFilter('companies', '1=1', {}, -1, 0, []);
for (const company of companies) {
// findOrCreate currency record for this company+date
}
});

src/routes/
├── (auth)/
│ ├── login/+page.svelte ← company CUI + username + password
│ └── logout/+server.ts
├── (app)/ ← all protected, layout checks session
│ ├── +layout.svelte ← sidebar nav, company name, user menu
│ │
│ ├── dashboard/+page.svelte ← KPI cards + alerts (see §5)
│ │
│ ├── products/
│ │ ├── +page.svelte ← searchable list, filter by category
│ │ ├── [id]/+page.svelte ← detail: info + stock by warehouse + movement history + SNs + supplier offers
│ │ └── new/+page.svelte
│ │
│ ├── clients/
│ │ ├── +page.svelte ← list with outstanding balance column
│ │ └── [id]/+page.svelte ← detail: invoices + payments + orders
│ │
│ ├── suppliers/
│ │ ├── +page.svelte
│ │ └── [id]/+page.svelte ← detail: purchase history + price offers
│ │
│ ├── sales/
│ │ ├── invoices/
│ │ │ ├── +page.svelte ← list: filter by status/client/date
│ │ │ ├── new/+page.svelte ← invoice editor (see §6)
│ │ │ └── [id]/+page.svelte ← view + PDF + e-Factura status + payment status
│ │ ├── orders/
│ │ │ ├── +page.svelte
│ │ │ └── [id]/+page.svelte ← view + fulfillment status + convert to invoice
│ │ └── proforma/ ← same as invoice but status=draft, no stock effect
│ │
│ ├── purchasing/
│ │ ├── invoices/ ← receive goods, update stock
│ │ │ ├── +page.svelte
│ │ │ └── [id]/+page.svelte
│ │ ├── orders/ ← send POs to suppliers
│ │ │ ├── +page.svelte
│ │ │ └── [id]/+page.svelte
│ │ └── supplier-offers/+page.svelte ← compare prices across distributors
│ │
│ ├── stock/
│ │ ├── +page.svelte ← current stock all products (filterable by warehouse)
│ │ ├── movements/+page.svelte ← full audit log
│ │ ├── serial-numbers/+page.svelte ← search SN, see history
│ │ └── transfers/+page.svelte ← move stock between warehouses
│ │
│ ├── treasury/
│ │ ├── +page.svelte ← cash balance + bank balance
│ │ ├── payments/new/+page.svelte ← record payment (in or out), link to invoice(s)
│ │ └── history/+page.svelte ← payment ledger
│ │
│ ├── warranty/
│ │ ├── +page.svelte ← list open service tickets
│ │ └── [id]/+page.svelte ← ticket detail, status updates
│ │
│ ├── licenses/+page.svelte ← active/expired software licenses sold to clients
│ │
│ ├── reports/
│ │ ├── outstanding/+page.svelte ← AR + AP aging (who owes what)
│ │ ├── stock-value/+page.svelte ← total inventory value by category
│ │ ├── sales/+page.svelte ← revenue by period / by product / by client
│ │ ├── margin/+page.svelte ← gross margin per product / invoice
│ │ └── top-products/+page.svelte ← best sellers by qty and by revenue
│ │
│ └── settings/
│ ├── company/+page.svelte ← name, CUI, IBAN, invoice series, logo
│ ├── users/+page.svelte ← manage users + roles
│ ├── warehouses/+page.svelte
│ ├── categories/+page.svelte
│ └── currencies/+page.svelte ← manual override + BNR sync trigger

graph TD
    subgraph DASH["Dashboard — loads on every login"]
        KPI1["💰 Today's sales\n(sum invoices_out today)"]
        KPI2["📦 Low stock alerts\n(products.stock < min_stock)"]
        KPI3["⚠️ Outstanding receivables\n(unpaid invoices_out > 30 days)"]
        KPI4["📋 Open orders\n(orders_out status=confirmed)"]
        KPI5["🔧 Open warranties\n(warranties status=open)"]
        KPI6["📅 Licenses expiring\n(< 30 days)"]
        FEED["Activity feed\n(last 20 documents)"]
    end

All KPI cards are single PocketBase queries. No complex aggregation needed.


sequenceDiagram
    participant U as User
    participant SK as SvelteKit
    participant PB as PocketBase
    participant ANAF as ANAF API

    U->>SK: Open /sales/invoices/new
    SK->>PB: GET /api/collections/terti?filter=type~'C'
    PB-->>SK: client list
    SK-->>U: render blank form

    U->>SK: type CUI in client field
    SK->>ANAF: GET /api/anaf/cui?cui=RO12345678
    ANAF-->>SK: {name, address, ...}
    SK-->>U: auto-fill client fields (or create new tert)

    U->>SK: add product line (type product name/code)
    SK->>PB: GET /api/collections/products?filter=name~'thinkpad'
    PB-->>SK: matching products with stock + list_price
    SK-->>U: show dropdown with stock indicator

    U->>SK: set qty, adjust price, apply discount
    SK-->>U: compute line_total + line_vat live (client-side)
    SK-->>U: show total_net, total_vat, total (grouped by VAT rate)

    U->>SK: POST confirm invoice
    SK->>PB: PATCH /api/collections/invoices_out/:id {status: "confirmed"}
    PB->>PB: hook fires — check stock, deduct, generate number
    PB-->>SK: {id, number: "MX-2026-0042", ...}
    SK->>PB: trigger PDF generation
    SK-->>U: redirect to /sales/invoices/MX-2026-0042
    U->>SK: click "Send e-Factura"
    SK->>ANAF: POST SPV OAuth2 + UBL XML
    ANAF-->>SK: upload_id
    SK->>PB: PATCH efactura_id + status=pending
- Product search: realtime as user types (debounced 300ms)
- Show stock indicator: ✅ in stock (qty) / ⚠️ low / ❌ out
- Show last purchase price vs current list price → margin %
- Serial number picker: if product.track_serial → show multi-select of available SNs
- VAT rate auto-filled from product, overridable
- Discount: per line, also supports per-client price list from price_lists collection
- Line totals update live without server round-trips

flowchart TD
    INV["Invoice confirmed\n(status = confirmed)"]
    XML["buildUBLXml(invoice)\n→ UBL 2.1 XML string\n(TypeScript, ~200 lines)"]
    VALID["ANAF UBL validator\n(optional pre-check)"]
    TOKEN["getAnafToken()\nOAuth2 client_credentials\nusing company's SPV certificate"]
    UPLOAD["POST to ANAF SPV\nhttps://api.anaf.ro/prod/FCTEL/rest/upload\nContent-Type: application/xml"]
    STORE["Save upload_id\nto invoices_out.efactura_id"]
    POLL["Poll ANAF every 5min\nGET /descarcare?id=upload_id\nuntil status = OK or NOK"]
    DONE["Update efactura_status\non invoice"]

    INV --> XML --> VALID --> TOKEN --> UPLOAD --> STORE --> POLL --> DONE

UBL XML structure for Romanian e-Factura:

src/lib/efactura.ts
export function buildUBLXml(
invoice: Invoice,
lines: InvoiceLine[],
client: Tert,
company: Company
): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>${invoice.number}</cbc:ID>
<cbc:IssueDate>${invoice.date}</cbc:IssueDate>
<cbc:DueDate>${invoice.due_date}</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyTaxScheme>
<cbc:CompanyID>RO${company.cui}</cbc:CompanyID>
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>${company.name}</cbc:RegistrationName>
<cbc:CompanyID>${company.reg_com}</cbc:CompanyID>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>
${buildBuyerParty(client)}
${lines.map((l, i) => buildInvoiceLine(l, i + 1)).join('\n')}
${buildTaxTotals(lines)}
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="RON">${invoice.total_net.toFixed(2)}</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="RON">${invoice.total_net.toFixed(2)}</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="RON">${invoice.total.toFixed(2)}</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="RON">${invoice.total.toFixed(2)}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>`;
}

All 20+ IT distributors share the same pattern — only the auth/format differs.

src/lib/suppliers/base.ts
interface SupplierAdapter {
name: string;
fetchOffers(): Promise<RawOffer[]>;
}
interface RawOffer {
supplierCode: string;
price: number;
stockQty: number;
currency: string;
}
// src/lib/suppliers/rhs.ts
export const RHSAdapter: SupplierAdapter = {
name: 'RHS',
async fetchOffers() {
const res = await fetch('https://api.rhs.ro/pricelist', {
headers: { Authorization: `Bearer ${env.RHS_TOKEN}` }
});
const data = await res.json();
return data.products.map((p) => ({
supplierCode: p.cod,
price: p.pret_fara_tva,
stockQty: p.stoc,
currency: 'RON'
}));
}
};
// Same adapter for ELKO, MICRO, ROYAL, etc. — just different URL/auth/field names
// pb_hooks/cron_suppliers.pb.js — runs nightly
cronAdd('supplier_feeds', '0 2 * * *', () => {
// for each active supplier with an adapter configured:
// 1. fetch offers
// 2. for each offer, find matching product_supplier_codes.supplier_code
// 3. upsert supplier_offers record with new price + stock
// this gives the "compare prices across distributors" view for free
});

9. What maxxcomputers Specifically Uses (Migration Checklist)

Section titled “9. What maxxcomputers Specifically Uses (Migration Checklist)”
  • Produse — product catalogue with supplier cross-codes
  • Terti — clients + suppliers (unified, typed C/F/CF)
  • CFI (cfi/) — customer invoices → invoices_out
  • FFI (ffi/) — supplier invoices → invoices_in
  • Comenzi clienti — customer orders → orders_out
  • Comenzi furnizori — purchase orders → orders_in
  • Trezorerie — payments, cash register, cheques → payments + payment_invoice_links
  • Stoc produse — stock movements + current stock (computed on product)
  • Serial numbers — tracked on IT hardware → serial_numbers
  • Garantii — warranty/service tickets → warranties
  • Licente (rapoarte/licente/) — software license sales tracking → licenses
  • Liste preturi — per-client price lists with discount % → price_lists
  • BNR rates — daily currency update → currencies + cron hook
  • ANAF CUI lookup — auto-fill tert on create → ANAF REST call in form
  • e-Factura — UBL XML + ANAF SPV submission → efactura.ts utility
  • Supplier price feeds — 20+ adapters → supplier_offers + nightly cron
  • Coletarie / Innight — shipment AWB tracking → shipments
  • PDF invoices — generate on confirm → file stored in PocketBase Files
  • Email notifications — invoice PDF to client, payment confirmation

Modules — Drop (not relevant to a computer shop)

Section titled “Modules — Drop (not relevant to a computer shop)”
  • Avicola / serii_crestere_pui — chicken farming
  • Mobilier — furniture manufacturing
  • Mifix — fixed assets (can add later if needed)
  • Salarii / payroll — use dedicated payroll software
  • Balanta / contabilitate — export data to accounting software (Saga, WinMentor)
  • Productie / asamblare — assembly production line (add later if needed)
  • Devize / situatii lucrari — project quoting (service orders module covers maxx)
  • D394 / SAF-T — accountant’s job, out of scope for v1
-- These tables from the maxx PostgreSQL DB map to new collections:
terti → terti
produse → products
produse_terti → product_supplier_codes
stocuri → initial stock record (one-time import + stock_movements seed)
cursvalutarbnr → currencies (last 12 months only)
garantii → warranties
-- invoices: import last 2 years for reporting history, not older
-- payments: import open (unpaid) balances only, not full history

DecisionChoiceReason
DatabasePocketBase (SQLite)Single tenant = one PB instance. Write serialization is a feature — prevents double-deduction of stock. No race conditions possible.
One instance per companyYesBetter isolation than shared DB. Scale via Docker/Fly.io. Breach of one tenant can’t touch others.
Business logic locationPocketBase JS hooks onlyLogic runs inside PocketBase transaction. SvelteKit routes are dumb — they only pass data. No split-brain possible.
Frontend frameworkSvelteKitMinimal boilerplate, excellent form actions, runs fine on Cloudflare Pages.
PDF generation@react-pdf/renderer server-side in SvelteKitRuns in Node/Cloudflare Workers, no headless browser needed for invoice-style PDFs.
EmailResend APIreplaces the broken SMTP setup, 1 API call, reliable delivery
Supplier feedsAdapter pattern, nightly cron in PB hooksEasily add/remove suppliers without touching core code.
Upgrade to PostgreSQLWhen neededSwap PocketBase DAO calls to a db.ts abstraction layer backed by Drizzle ORM. Hooks become PostgreSQL triggers or Supabase edge functions. SvelteKit routes are untouched.

graph LR
    subgraph Dev["Development"]
        TS["TypeScript"]
        SVELTE["SvelteKit 2"]
        TAILWIND["Tailwind CSS"]
        SHADCN["shadcn-svelte\n(UI components)"]
    end

    subgraph Runtime["Runtime"]
        CF["Cloudflare Pages\n(SvelteKit SSR)"]
        PB["PocketBase\n(Fly.io / VPS)\nSQLite + JS hooks"]
    end

    subgraph Services["External Services"]
        RESEND["Resend\n(email)"]
        R2["Cloudflare R2\n(PDF archive)"]
        ANAF2["ANAF APIs\n(CUI + SPV)"]
        BNR2["BNR XML\n(rates)"]
    end

    Dev --> Runtime
    Runtime --> Services

maxx-erp/
├── src/
│ ├── routes/ (see §4 Route Map)
│ ├── lib/
│ │ ├── pb.ts — PocketBase client singleton + typed collections
│ │ ├── db.ts — abstraction layer over PocketBase (swap for Drizzle later)
│ │ ├── efactura.ts — UBL XML builder + ANAF SPV client
│ │ ├── bnr.ts — BNR XML parser
│ │ ├── pdf/
│ │ │ ├── InvoiceTemplate.tsx
│ │ │ └── render.ts — renderToBuffer wrapper
│ │ ├── suppliers/
│ │ │ ├── base.ts — SupplierAdapter interface
│ │ │ ├── rhs.ts
│ │ │ ├── elko.ts
│ │ │ └── ... — one file per distributor
│ │ ├── vat.ts — Romanian VAT computation (19/9/5/0, rounding rules)
│ │ ├── document-number.ts — series + sequential number formatting
│ │ └── anaf.ts — CUI lookup client
│ └── app.d.ts — Locals type (user, company, pb instance)
├── pb_hooks/ — PocketBase JS hooks (business logic)
│ ├── invoices_out.pb.js
│ ├── invoices_in.pb.js
│ ├── orders_out.pb.js
│ ├── payments.pb.js
│ ├── serial_numbers.pb.js
│ ├── cron_bnr.pb.js
│ └── cron_suppliers.pb.js
├── pb_migrations/ — PocketBase schema migrations (JSON, auto-generated)
└── scripts/
├── migrate-from-ancora.ts — one-time data import from old PostgreSQL DB
└── seed.ts — dev data seeder