All projects
Case study
Partner & affiliate platform

BodySpa Partners

A partner program for beauty salons that plugs into an existing CloudCart store — codes, commissions and supplier reports on autopilot.

partners.bodyspa.bg
BodySpa Partners
Role
End-to-end development: analysis, architecture, design, frontend, backend, integration, deployment
Client
BodySpa (bodyspa.bg), an online store for professional cosmetics
Status
Running in production, verified with real orders
Published
Stack
React 19TypeScriptViteTailwind CSSNode.js / ExpressMySQLCloudCart APIpdfkitExcelJSCloudflare TurnstileResend
At a glance
~700
products from 8 brands mapped to suppliers
5 → ∞
discount limit → unlimited partner codes
7
webhooks managed by an idempotent script
2FA
custom TOTP implementation

The bodyspa.bg online store existed before our involvement and we did not build it. Our work is the standalone partner system that connects to it.

01

Context

BodySpa sells professional cosmetics online. The store runs on CloudCart, a Bulgarian SaaS e-commerce platform. The company wanted to attract beauty salons as partners. A salon recommends the store to its clients, the clients shop with the salon’s promo code, and the salon earns a commission on the sales.

Before the system, all of this could only be managed manually: creating codes, checking which orders used which code, calculating commissions in Excel and sending reports to suppliers. With more partners the manual process became unmanageable and error-prone.

02

Goals

  • Salons apply online, and an admin approves them with a single action.
  • An approved partner automatically gets a working promo code in the store.
  • Every order with a promo code is linked to the right partner, and the commission is calculated according to the rules in the contract.
  • Partners see their orders and commissions in real time.
  • Admins get monthly and per-supplier reports, and suppliers automatically receive reports on their sales.
  • All of this works without any changes to the store itself.
03

Constraints

The project had several serious constraints that shaped many of the technical decisions:

  • A closed SaaS platform. We had no access to the store’s code or database. The only channel was the CloudCart REST API and its webhooks, and their documentation is incomplete.
  • Discount limit. The store’s plan allows only 5 separate discounts, while the system had to support an unlimited number of partners.
  • Shared hosting. The server runs on cPanel (Passenger) with limited resources (CloudLinux LVE). A headless browser can’t run there, WebAssembly exhausts the memory, and the database user has no DELETE privilege.
  • Financial data. A mistake in commissions means real money owed to real people. The system had to be accurate and auditable.
04

Approach

We started with a technical spike to remove the main risks: We connected to the CloudCart API and verified against the real store that we could create codes, read orders and find out which code was used.

Then we built the entire frontend on a mock API. Every screen was shown to and approved by the client before a backend existed. All communication goes through a single module (api/index.ts). When the backend was ready, we changed only that module, without touching a single screen.

Finally we built the backend, the integration and the automations. Every feature was verified against the live store, not just locally.

05

Technical challenges and solutions

1. Reverse-engineering an undocumented API

CloudCart’s documentation doesn’t reliably describe key details, so we tested and documented the API’s actual behavior. We found that amounts are in whole cents with VAT included, that some fields (like the customer group) are read-only and are changed through a separate relationship endpoint, and that dates require an exact format.

The most important finding was how an order is linked to the code used. The discounts field on the order comes back empty even when a discount was applied. The actual code lives in a separate order-discount resource, in the codes[] array, and has to be grouped by its own order_id. Without this finding, recognizing the partner behind an order wouldn’t have worked.

2. The 5-discount limit

The initial approach (a separate discount per partner) hit the plan’s limit. The solution was a container discount: one discount in CloudCart holding an unlimited number of codes. We verified it by creating more codes than the limit without any issue.

This created a new problem: all codes share the same discount ID, so an order can’t be linked to a partner by ID. So the partner is identified by the code text. Every partner gets a unique branded code (e.g. SITEDEN10: salon name + discount percentage).

3. Self-healing automatic code creation

When a partner is approved, the system creates the code in CloudCart and moves the partner’s customer account into the “Partners” group for wholesale prices. The logic is idempotent and self-healing:

  • if the code already exists in CloudCart, it is reused;
  • if the code has disappeared from the store, it is created again;
  • the code is saved to the database only after CloudCart confirms its creation.

The last rule wasn’t theoretical. In production we found that a missing config variable had caused the system to save codes to the database without creating them in the store. The bug gave no signal at all. Besides the fix, we added a health endpoint that shows whether key settings are active, so a problem like this can’t go unnoticed again.

If the partner doesn’t have a store account yet, the group move happens later, automatically, via a webhook when they register.

4. Two-way sync without an infinite loop

The commission percentage and the discount for the partner’s clients are the same number. A change in the admin panel updates the code’s value in CloudCart, and on approval the value is read from the store.

We deliberately didn’t use webhooks for discount changes. The panel is the source of truth: if a write from the panel triggers an event, and the event triggers another write, the system enters a write → event → write loop.

5. Reliable webhook handling

CloudCart has two quirks that make webhooks unreliable as the only source of data:

  • it deactivates a webhook on the first failed response (anything other than 2xx);
  • there is no event for a deleted order.

The solution has several layers:

  • Store before processing. Every incoming event is saved raw in the database, after which the server always responds with 200. Processing errors are logged but don’t cause the webhook to be deactivated.
  • Registration by script. The seven webhooks (orders, products, customers) are registered with a script that can be run repeatedly. It creates the missing ones and fixes the URL, key and version of existing ones. There is also a mode that creates new webhooks inactive, to prepare the store before the matching code is deployed.
  • Sync. A separate process re-fetches orders from the API. The record is updated by order ID and the commission is recalculated, so the process can safely be run as many times as needed. It covers deleted orders, outages and data from before the system existed.

During a review we found that in production the webhook endpoints had been open to anyone: the secret key existed in the config but was empty, and the check let requests through. We generated a key and registered it as an HTTP header on all webhooks — a header rather than a URL parameter, so it doesn’t end up in server logs.

6. Calculating commissions

The commission rules come straight from the contract with the client:

Order statusCommission
Processing / shippedexpected
Completedconfirmed, payable
Cancelled / returneddropped
Partially returnedrecalculated on the remainder

The commission is calculated on the net amount, excluding VAT and shipping. Partial returns are read from a separate API resource. When the returned amount can’t be determined with certainty, the system doesn’t guess: the order gets a zero commission and is flagged for manual review.

7. Historical data and changes over time

The cost price and supplier of every product are snapshotted at the time of the order. So a later price change doesn’t rewrite past reports.

This created the opposite problem. When a product is linked to a supplier for the first time, older orders still count as “no supplier”. So every change to the mapping automatically updates the already recorded rows and shows how many rows were affected.

Suppliers are mapped by brand, because that’s how BodySpa actually works: about 700 products from 8 brands. One click creates a supplier for each brand. Cost prices are imported from an Excel file, and the parser accepts both variants of the column headers, so a file the client had already filled in doesn’t break after a rename.

8. Reports that match

During a review we found that the monthly report and the file sent to suppliers used a different base: one gross, the other net. The two documents could never match, and the per-supplier breakdown didn’t add up to the total.

After a decision by the client, both documents now use the same formula and match to the cent. The commission stays net as per the contract, and the report says so explicitly. Orders without line items, which can’t be split by supplier, are shown separately instead of silently disappearing into the total.

9. Cyrillic PDFs without a browser

The hosting doesn’t allow a headless browser, so PDF reports are generated directly with pdfkit. All built-in PDF fonts are WinAnsi and render Cyrillic as garbage, so we embedded Montserrat (the panel’s font) and verified coverage for Cyrillic and the € sign. If the fonts are missing, the endpoint returns a clear error instead of a broken file.

We checked the output visually in three scenarios (a normal report, 22 suppliers across 3 pages, an empty report) and found and fixed two defects: an empty last page and long names wrapping onto the neighboring row. The report is styled with the panel’s design tokens and has “page X of Y” numbering.

10. Automatic supplier reports

Each supplier receives a CSV with their own sales for the period by email, and the admin gets a summary copy. The schedule (daily or weekly, on a chosen day) is changed from the panel. There is only one daily cron job: the script reads the settings and exits if today isn’t a sending day, so changing the schedule doesn’t require touching the server.

The period always ends with yesterday, so a report is never sent for a day that is still receiving orders. Every send attempt is logged, and the panel shows the latest sends and has a “Send now” button.

11. From QR code to applied discount

Every partner has a QR code to print and display in the salon. When a client scans it, the store opens with the code in the URL. A script added to the store remembers the code and applies it automatically in the cart.

The script doesn’t call the API directly — it fills in the code field and clicks the “Apply” button, going through the store’s own form. This way CloudCart’s CSRF protection, session and validation work exactly as with manual entry.

12. Working within the hosting limits

  • Node’s built-in fetch uses a WebAssembly parser that exhausts the memory limit and crashes the process. All external HTTP requests go through a custom client built on node:https.
  • The database user has no DELETE privilege, which in production silently broke saving order line items. Order processing was rewritten to never delete at all.
  • The deployment process is documented step by step for cPanel/Passenger.
06

Features

Partner panel

  • A dashboard with KPIs, custom SVG charts (area, bar, donut) with interactive tooltips and a commission trend
  • Promo code and QR code to download and print
  • Order history with filters and a detail view: status timeline and commission calculation
  • A wholesale price catalog visible only to partners
  • Profile, documents, a “How it works” guide and a rotating image banner

Admin panel

  • An overview dashboard, an approval queue and top partners
  • Partner management: approve, reject and suspend, including in bulk
  • Amounts owed to each partner (confirmed and expected)
  • All orders, the catalog, brand-to-supplier mapping and cost price import
  • Monthly reports with PDF, supplier management and automatic reports
  • An editor for the automatic email templates (with variables like {name} and {code})
  • An audit log of every action: who approved or changed what, and when
  • Formatted Excel export (currency, percentages, dates, a totals row, frozen headers, filters), loaded only on demand

Security

  • A custom TOTP two-factor authentication implementation: mandatory for admins, optional for partners, with a backup code by email
  • bcrypt, JWT sessions, temporary lockout after failed login attempts
  • Password reset via a one-time token stored hashed
  • Cloudflare Turnstile against bots, validation of all inputs with zod, webhooks protected with a key

Interface

  • Fully in Bulgarian, with centralized copy and correct plural forms
  • A custom design system (gold, ink, warm white): modals, side panels, toasts, skeleton screens
  • Responsive design for mobile devices
07

Result

The platform runs in production and has been verified with real orders. Orders are linked to the right partner and commissions are calculated correctly. A process that used to require manually creating codes, tracking orders in spreadsheets and sending reports is now automated — from the partner’s application to the email to the supplier.

Want a system like this for your business?

Tell us what eats your team’s time. In 30 minutes we’ll show you what can run on its own.

Book a free call
Next project
Influ-Link
Two-sided marketplace