Hulo Global
Plugins Roadmap

Business Credit changelog

Every release of @huloglobal/vendure-plugin-business-credit. Latest release: v0.3.1 — 18 September 2026.

v0.3.1

18 September 2026 Latest
Changed
  • Vendure plugin directory readiness. The plugin class, every service and the BusinessCreditEvent carry @category JSDoc tags (Plugin / Services / Events); the runtime compatibility declaration now matches the tested range >=3.5.0 <4.0.0 instead of ^3.0.0.
  • README: npm, download, Vendure and database badges, a five-step *Quick start* at the top (including the admin-UI compile step and the payment-method setup) and a *Compatibility* section. No functional changes.

v0.3.0

16 September 2026
Added
  • Multi-user company accounts (members). The account holder (owner) adds colleagues by e-mail as buyer (may order on account) or viewer (invoices and statements only). Every storefront route, the Pay-on-Account handler and the eligibility checker now resolve "my account" through AccountsService.resolveAccountForCustomer — the customer's own account, else the one they are a member of. A viewer at checkout is declined with member_viewer ("Your account role does not allow placing orders on account"). Admin: GET/POST /business-credit/accounts/:id/members, POST accounts/:id/members/:memberId { role }, DELETE accounts/:id/members/:memberId. Storefront: GET /business-credit/my/members (owner or buyer), POST my/members { email, role } and DELETE my/members/:memberId (owner only; per-channel switch allowMemberManagement); GET my/account gains role, isOwner and memberManagement. Refusals: 404 no_customer, 409 is_owner / already_member / owns_account, 400 bad_role. Events member.added (e-mails the new member), member.role_changed, member.removed. Table business_credit_member. Premium members.
  • Invitations. Offer an account on pre-agreed terms without an application: POST /business-credit/invitations { email, creditLimitMinor, termsDays?, companyName?, message? } e-mails the invitee a 64-hex-character token link (<storefront>/account/credit/accept?token=…) that expires after the channel's invitationDays (default 14). GET invitations?status&email, POST invitations/:id/revoke, POST invitations/:id/resend (bumps sentCount). A public GET /business-credit/invitations/:token feeds the accept page with the terms and a masked e-mail (in***@example.com), never the token. The invitee signs in with the same address and calls POST /business-credit/my/invitations/accept { token } → an active account on the invited terms, approvedBy: 'invitation', first review one cycle out; the merchant's notify address gets invitation_accepted. Refusals: 403 email_mismatch, 409 invitation_expired / invitation_not_pending / account_exists / invitation_pending. The daily pass expires stale invitations (invitationsExpired in the report). Table business_credit_invitation. Premium invitations.
  • Auto-approval. With autoApproveEnabled a storefront application is approved on the spot (actor auto, note "Auto-approved by rule") when the customer has at least autoApproveMinOrders settled orders worth autoApproveMinSpendMinor in the channel and — if autoApproveRequireCompanyNumber — gave a company number. The limit granted is the requested one (or the channel default) capped at autoApproveMaxLimitMinor; a rule whose cap is 0 never approves. POST /business-credit/my/apply answers { application, account, availability, autoApproved }. Anything that fails a check stays pending for a human. Premium auto_approval; pure rule in src/accounts/rules.ts.
  • Credit reviews. Every approval sets reviewAt = now + reviewEveryMonths (default 12; 0 = off). Accounts carry reviewAt, reviewNote and reviewDue; GET accounts?reviewDueOnly=true and the dashboard's reviewsDue list what is waiting. The daily pass publishes account.review_due and e-mails review_due to the notify address (once, then again after 30 days). POST /business-credit/accounts/:id/review { note? } records the review and moves reviewAt on one cycle (from today when it was long overdue). Event account.reviewed.
  • Payment plans. Split an open invoice's balance into instalments: POST /business-credit/invoices/:id/plan { instalments: 3 | [{ dueAt, amountMinor }], startAt?, intervalDays?, note? } (equal split with the last instalment taking the rounding, 2–60 instalments, default 30 days apart). Money allocated to the invoice pays the instalments in order; the plan completes when the invoice is paid and reopens on a refund. While a plan is active the daily pass judges lateness by the next unpaid instalment — marks it overdue, sends the reminders against its due date (with an "instalment 2 of 3" line), charges no late fee, and counts auto-suspend from the overdue instalment. GET invoices/:id/plan, POST invoices/:id/plan/cancel { note? } (the invoice goes back to overdue when it is past its own due date), GET plans?channelId&accountId&invoiceId&status. Customers see the schedule on GET /business-credit/my/invoices/:id (plan) and can ask for one with POST my/invoices/:id/plan/request { instalments, message? } → 202 and a plan_requested e-mail to the merchant (per-channel switch allowCustomerPlanRequests). Events plan.created (e-mails the schedule), plan.requested, plan.completed, plan.cancelled, plan.reopened. Report gains planInstalmentsOverdue. Table business_credit_plan. Premium payment_plans.
  • Reward points (loyalty) programme. Per-channel config (GET/POST /business-credit/loyalty/config): enabled, programmeName, earnPointsPer100Minor, redeemValueMinorPerPoint, minRedeemPoints, maxRedeemPercent, expiryMonths, signupBonusPoints, earnOnAccountOrders, tiers (default Bronze / Silver 2 000 / Gold 10 000 at ×1 / ×1.25 / ×1.5). Points are earned once per order on PaymentSettled (and on OrderPlacedEvent for orders that settle at once, de-duplicated): floor(totalWithTax / 100 × rate × tierMultiplier); a tier change e-mails tier_changed; cancelling the order writes a reversal (clamped at zero) and releases any redemption. Redeeming at checkout: POST /business-credit/my/loyalty/redeem { points } validates the minimum, the balance and the maxRedeemPercent cap of subTotalWithTax (400 below_minimum / insufficient_points / over_max_percent with maxPoints), then adds a negative surcharge (sku LOYALTY-POINTS, tax-inclusive) to the active order and books a pending redeem; POST my/loyalty/unredeem removes it; placing the order finalises it; a second redeem replaces the first. GET my/loyalty → balance, value, tier, next tier, expiring-soon, earn rate, redeem rules, pending redemption; GET my/loyalty/transactions; public GET /business-credit/loyalty/preview?amountMinor= for product pages (the shopper's tier when signed in). Admin: accounts (search, tier), account detail, adjust (positive adjustments count towards tiers), transactions, stats, top-customers, export.csv (premium exports), POST loyalty/run { expire? }. LoyaltyCron at 03:30 on the worker releases stale pending redemptions (7 days) daily and expires points on the 1st: expirable = max(0, min(balance, Σ expired credits − Σ every deduction)), oldest points consumed first. Tables business_credit_loyalty_config, business_credit_loyalty_account, business_credit_loyalty_tx. Premium loyalty; when unlicensed or switched off nothing is earned and the storefront reads { enabled: false }.
  • E-mails. Seven new kinds: invitation, invitation_accepted, member_added, plan_created, plan_requested, review_due, tier_changed — all with preview and test-send.
  • Events. account.created, invitation.created / accepted / revoked / expired, member.added / role_changed / removed, account.reviewed, account.review_due, plan.*, loyalty.tier_changed, loyalty.adjusted, loyalty.config_updated.
  • Channel config gains autoApproveEnabled, autoApproveMaxLimitMinor, autoApproveMinOrders, autoApproveMinSpendMinor, autoApproveRequireCompanyNumber, reviewEveryMonths, invitationDays, allowCustomerPlanRequests, allowMemberManagement.
  • A second MariaDB end-to-end suite (e2e/business-credit-platform.e2e-spec.ts) covering invitations, members and roles at checkout, auto-approval, reviews, payment plans through dunning and cancellation, plan requests, the reward programme end to end (earning, tiers, redeem / unredeem / replace / finalise, cancellation reversal, on-account earning, admin stats and exports, expiry) and the unlicensed behaviour of every new surface.
Fixed
  • GET /business-credit/my/account offered canApply: true to a customer who already trades through a company account they are a member of; it now considers the resolved account, not only the customer's own.
  • addColumnIfMissing looked up information_schema.columns without restricting it to the current database, so on a server hosting another database that already had a column (staging next to production, a second Vendure instance) the upgrade ALTER TABLE was skipped and the new column never appeared. The lookup is now scoped to the current database (DATABASE() on MySQL / MariaDB, current_database() + current_schema() on PostgreSQL).

v0.2.0

16 September 2026
Added
  • Prepaid funds. Money held on an account before any invoice exists. An admin deposit (POST /business-credit/accounts/:id/deposit { method, amountMinor, reference?, receivedAt?, note? }; real payment methods only) or a customer card top-up through Stripe Checkout (POST /business-credit/my/top-up, admin-generated link via POST /business-credit/accounts/:id/top-up-link; premium card_pay_links) is applied to any open invoice at once and the remainder held as funds, raising the available credit. Withdraw (POST /business-credit/accounts/:id/withdraw, 409 insufficient_funds above the balance) books the return on the ledger. GET /business-credit/accounts/:id/funds → { fundsHeldMinor }.
  • Automatic settlement from funds. The settlements service subscribes to invoice.issued and applies held funds the moment an invoice is raised, so an order placed on account against funds is paid immediately (settlement.credit_applied + invoice.paid with viaCredit). Funds count towards eligibility, so an account with a zero credit limit can trade prepaid only.
  • Top-up webhook. Checkout Sessions carrying { business_credit: '1', topup: '1', accountId, channelId, amountMinor } (client_reference_id BC-<id>-TOPUP) are recorded by the existing signed webhook as a card settlement with no allocations, then applied to open invoices; the outcome carries topUp: true. Replays of the same payment_intent remain duplicates. parseBusinessCreditMetadata now returns topUp; new buildTopUpSessionParams.
  • Storefront. GET /business-credit/my/account includes funds: { heldMinor, currencyCode }; new GET /business-credit/my/settlements lists the customer's payments, deposits and top-ups with fundsHeldMinor.
  • Admin UI. A *Funds* pane on the account detail: funds held, available and open balance, a card top-up link (premium-locked like pay links, copied to the clipboard), *Record a deposit* (method limited to real payment methods, amount, reference, date, note) and *Withdraw funds* (amount, note, confirmation). The Overview KPI is now *Funds held*; settlement rows with no invoice allocations are labelled *Funds top-up / deposit*.
  • E2E coverage for deposits, settlement of a new on-account order from funds, the top-up webhook (with replay), withdrawals and the 409 refusal, the customer's settlement list, and a prepaid-only account (limit 0, funds 200.00: a 120.00 order is eligible and settles, 400.00 is not).

v0.1.0

16 September 2026
Added
  • Credit accounts. One account per customer per channel with a credit limit, net terms, grace days, purchase-order requirement, company / VAT details, billing email and an accountRef (BC-000007). Statuses pending, active, suspended, closed (closing needs zero exposure). Permanent and temporary limit changes (with expiry) recorded in business_credit_limit_change; every state change written to the business_credit_event audit log and, for the customer-facing ones, published as a BusinessCreditEvent on Vendure's EventBus.
  • Applications. Storefront applications (POST /business-credit/my/apply, premium; per-channel on/off), admin-created applications, an approve / reject queue that creates or updates the account, customer withdrawal, and the received / approved / rejected emails.
  • Pay on Account. A business-credit payment handler (settles the order at once, raises the invoice, returns the invoice details in metadata.public, records a credit note on refund) and a business-credit-eligibility checker (active account, enough available credit, no overdue hold, optional minimum order) whose reason strings surface as Vendure's eligibilityMessage / paymentErrorMessage. Purchase-order number from the addPaymentToOrder metadata or an Order custom field.
  • Invoices. Per-channel numbering (INV- prefix + zero-padded sequence, contention-safe), dueAt = issued + terms, statuses open / part_paid / paid / overdue / written_off / void, late fees, reminders-sent tracking, pay-link reference; admin listing with search and filters, customer listing scoped to their own account.
  • Settlements. Bank transfer, card, direct debit, cheque, cash, credit note and write-off; oldest-due-first or explicit allocation; unallocated money held as account credit and applied by the daily pass or on demand; settlement refunds (unallocated first, then proportional); invoice void, write-off and manual fees; a signed ledger (charge, payment, credit_note, write_off, fee, adjustment, refund) whose sum is the account balance; idempotency on providerRef.
  • Stripe pay links. Checkout Sessions over plain fetch (no SDK) from the admin or the storefront, keyed by the channel's existing Vendure Stripe payment method; a raw-body, signature-verified webhook (POST /business-credit/stripe-webhook) for checkout.session.completed / checkout.session.async_payment_succeeded that records the card settlement exactly once and answers 5xx on internal failure so Stripe retries. Per-channel secrets via BUSINESS_CREDIT_STRIPE_WEBHOOK_SECRET_<CHANNELCODE>.
  • Statements. Opening balance, running lines, closing balance, debit / credit totals, open invoices and 1–30 / 31–60 / 61–90 / 90+ aging for any period, as JSON, HTML and text; admin and customer endpoints.
  • Dunning. A daily pass (07:00 on the worker, crons.dailyAt / crons.enabled, admin *Run now*) that expires temporary limits, marks invoices overdue after grace, sends reminders on a per-channel schedule relative to the due date (default -3, 0, 7, 14, 30; one email per invoice per run, earlier days caught up silently), charges a pro-rata monthly late fee in 30-day chunks after a fee grace period, auto-suspends accounts past a threshold, applies held credit and emails last month's statement on the statement day; a report with skipped counts on the free tier.
  • Emails. Ten kinds with {{var}} templates in a branded HTML shell; SMTP from the smtp option or SMTP_* env; preview and test-send from the settings tab; a test seam (MailerService.setSender) for hosts and the e2e suite.
  • CSV exports of invoices, settlements and an account ledger with running balance (premium).
  • Admin dashboard at *Sales → Business credit*: Overview, Accounts (detail with availability, limit history, invoices, ledger, record settlement, statement, events), Applications, Invoices, Settlements, Settings, and the Licence & billing card (14-day evaluation, buy from the admin, billing portal, self-update).
  • Storefront REST under /business-credit/my: account and availability, apply / application / withdraw, invoices, pay links, statement, ledger.
  • Free tier: accounts, limits with audit trail, Pay-on-Account handler and checker, invoices, manual settlements and allocation, ledger, statements, dashboard. Premium: Stripe pay links, reminders and dunning emails, late fees, auto-suspend, statement emails, CSV exports, storefront applications.
  • MariaDB end-to-end suite covering the whole flow (application → approval → order on account → over-limit refusal → bank-transfer settlement → temporary limit and expiry → overdue, reminder, late fee and auto-suspend → reactivation with overdue hold → Stripe webhook with duplicate delivery → refund as credit note → CSV exports → statement balancing to the ledger).