Wix won’t do the bank half of this from your site. There’s no API for opening folders inside the business account or moving money between them, so no amount of code gets you five sub-balances.
The ledger half you can have now, and it is the half that costs you the five transfer fees. If something inside your site tracks what each share of the balance is for, four of those five envelopes never have to leave the account. The money stays in one place and you make one real transfer, for whatever has to sit somewhere else.
Two collections do it. Envelopes holds a name, a percent and a running balance. Allocations holds one row per payment, which is what lets you audit it later and reverse a refund properly.
The first thing that will trip you up is that wixStores_onOrderPaid is deprecated and the docs now point at onOrderPaymentStatusUpdated() in wix-ecom-backend. The handler goes in backend/events.js:
import wixData from 'wix-data';
const ENVELOPES = ['Investment', 'Savings', 'Overhead', 'Payroll', 'Personal'];
const PERCENTS = [12, 13, 29, 36, 10];
export async function wixEcom_onOrderPaymentStatusUpdated(event) {
const order = event.data.order;
if (order.paymentStatus !== 'PAID') return;
if (event.data.previousPaymentStatus === 'PAID') return;
// priceSummary amounts are strings ("40.00", "10.0", "0"), and the sales
// tax in there is not your money, so it should not be split.
const cents = (s) => Math.round(parseFloat(s || '0') * 100);
const net = cents(order.priceSummary.totalPrice.amount)
- cents(order.priceSummary.tax.amount);
const parts = split(net, PERCENTS);
await wixData.insert('Allocations', {
orderId: order._id,
eventId: event.metadata.id,
paidAt: order._updatedDate,
netCents: net,
percents: PERCENTS,
parts
}, { suppressAuth: true });
for (let i = 0; i < ENVELOPES.length; i++) {
const { items } = await wixData.query('Envelopes')
.eq('name', ENVELOPES[i]).find({ suppressAuth: true });
const env = items[0];
env.balanceCents = (env.balanceCents || 0) + parts[i];
await wixData.update('Envelopes', env, { suppressAuth: true });
}
}
The split itself is worth doing carefully:
function split(cents, percents) {
const total = percents.reduce((a, b) => a + b, 0);
const exact = percents.map((p) => (cents * p) / total);
const parts = exact.map(Math.floor);
const short = cents - parts.reduce((a, b) => a + b, 0);
exact.map((v, i) => ({ i, frac: v - Math.floor(v) }))
.sort((a, b) => (b.frac - a.frac) || (a.i - b.i))
.slice(0, short)
.forEach((x) => parts[x.i]++);
return parts;
}
Rounding each percentage on its own is what breaks this. Five independently rounded shares don’t add up to the payment that came in, and the gap accumulates. Handing the leftover cents to the largest fractional shares means the five parts sum to the payment exactly, every time, with no line absorbing the difference.
Three more things will catch you out. Refunds and chargebacks have to come back out of the envelopes they went into, at the percentages that applied when that money arrived, so if you retune the split in July and refund a May session in August, clawing back at July’s percentages puts the money in the wrong envelopes and the total still reconciles. Backend events don’t fire in Preview, so the site has to be published before you can test any of it. And if you want a monthly statement out of the Allocations rows, a scheduled job in backend/jobs.config will write one, but the times in that file are UTC, not your local time.
If your money mostly arrives through Bookings rather than Stores, the event is a different one, but everything after it is the same. Log the event object once and read the property names off what actually arrives before you build against them.
I haven’t run this against a live business account, so treat it as a starting point rather than something already proven.