Models: the OnlyFans transaction summary, and an id integrations can join on¶
Models → Overview gains a third tab that reconciles OnlyFans transactions across every model in the range — on screen, as a CSV and as a branded PDF. For integrations, each OnlyFans account in the public roster now carries the platform's own id for it.
The quarterly OnlyFans reconciliation, without the spreadsheet¶
Building the quarterly transaction summary meant exporting transactions out of OF Manager, deduplicating them, pivoting them four ways and pasting the result into a document. Every figure in that document was already in HRIS.
Models → Overview → OnlyFans does the pivoting, over the cutoff range already picked at the top of the screen, across every model.
- Headline tiles for transactions, gross, fee, net, VAT, tax and media tax, with the settled / pending / refunded split beside them.
- By period, switching between cutoffs and months.
- By model and by sales type, each with count, gross, net and share of gross.
- Export → CSV puts all four blocks in one file. Export → PDF renders the branded report, in the same brand as a payslip.
It will not match the Net revenue figure on the Models tab, and the tab says so above the numbers. The two are counted from different places on purpose: transactions are the audit trail, and every other earnings figure in HRIS comes from the daily rollup. Refunds also count into gross here, because an OnlyFans refund carries a positive amount and is marked by its status rather than its sign — the status split is what separates them. Read this tab to reconcile against OnlyFans; read the Models tab for what the agency earned.
Guide: OnlyFans
Also¶
- The per-model table on Models → Overview → Models sits on a white surface again. It had no background of its own, so the page showed through it and the table read as faintly grey.
Public API (for integrations)¶
Each account in the model roster now carries platform_user_id — the
platform's own id for that account, which for OnlyFans is its numeric
profile.id.
GET /api/public/v1/models/
accounts[] → { platform, platform_name, handle, platform_user_id, is_active }
It is an integer. It is null for a platform with no ingest — everything but
OnlyFans today — and for an OnlyFans account linked before the id was captured.
The key is always present, so null never has to be told apart from a field
that isn't there.
Why it matters. /income/ has always grouped by day (?group_by=day), but
every row is keyed on the HRIS model id. Joining those rows to your own
OnlyFans records meant matching on handle — the OnlyFans username — which
changes when a model renames, and silently re-points the join at whoever took
the name. platform_user_id doesn't change.
Reversed later the same day
This post originally said the account's other upstream id — the one our
ingest vendor files it under — was "still not published and won't be".
That is no longer true: it is published as aruna_of_api_id. See
the follow-up post for which id to join on.
The reference is in the app, at Profile menu → HRIS API. It is generated from the API itself, so it already shows the new field. Reading it needs an HRIS account — a partner who holds only a key may not have one.
Guide: The API reference
Technical changes¶
For developers and whoever deploys. None of this needs a guide.
Data model
- No new tables, columns or constraints.
platform_user_idreads the existingModelPlatformAccount.external_user_id, populated byagency_models/services/of_link.py; the transaction summary aggregatesModelTransaction, which the nightly ingest already writes.
Behaviour
- Two new reads under
agency_models:GET /api/models/overview/of-transactions/andGET /api/models/overview/of-transactions.pdf/(?group=cutoff|month). Both resolve their range through the same_overview_rangehelper as/overview/, so the tabs cannot end up reporting different periods. Five SQLGROUP BYs, on an endpoint of their own rather than inside/overview/, so the default tab doesn't pay for them. - Months truncate in UTC, not
TIME_ZONE. The column iscreated_at_utc, and Manila truncation would walk a 31st-at-23:00 row into the next month. - Percentages are served at four decimal places (
max_digits=9, decimal_places=4) and rounded once, at the client. At two, a real share rounded to0.00and the renderer could no longer tell it from a model that earned nothing; those rows now read<0.1%. PublicModelAccountSerializergainsplatform_user_id— integer, nullable,source="external_user_id". The public schema marks it required and nullable. The app schema is untouched, becausepreprocess_exclude_non_apikeeps/api/public/out of it, so no client regeneration is needed.core/pdf.pynow holds the brand palette, logo and section band, moved out offinance/services/invoice_pdf.py. Mechanical — same values, noPDF_LAYOUT_VERSIONbump, so cached invoices are unaffected.
Deploy
- No migrations. No new environment variables, no new workers, no one-off commands.
Tests
agency_models/tests/test_of_transaction_summary.py— 28 tests over the aggregates, refund handling, both groupings and the rendered PDF.integrations/tests/test_public_models.pypins the id split: the platform's own id is published, the vendor's row id is not.