Admin operations guide
Step-by-step workflows for each operation in the commissions module. Everything from the admin portal at /admin/commissions/*.
1. Create a sales rep
Route: /admin/commissions/vendedores → "New rep" button
Required fields:
- Full name
- Email (unique in the system)
Optional fields:
- Phone, Tax ID, Country
- Payment method:
ach,wire,check,paypal,wise,other - Payment details: banking info or account (JSONB — routing number, account, PayPal email, etc.)
- Ref code: slug for landing URLs. If left empty, auto-generated from the name (
maría gonzález→maria-gonzalez). - Internal notes
Default terms (collapsible section):
- Recurring %: percentage applied to revenue
- Recurring fixed (cents): fixed monthly amount independent of revenue
- One-time amount (cents): bonus for each tenant brought in
- Basis:
gross(on gross revenue) ornet(deducting Stripe fees)
These defaults are applied in cascade when creating assignments: if a field isn't specified when creating the assignment, the rep's default is used; if the rep also has no default, the global one in commission_settings is used.
Result: Rep created with status='active'. Appears in the list and in the assignment picker.
2. Assign a tenant to reps
Route: /admin/commissions/asignaciones → "New assignment"
Steps:
- Select the tenant (combobox grouped by status: active, past_due, etc.).
- Add participants: editable table with columns (rep, share %, role).
- "+" button to add a row.
- The sum of shares must be exactly 100. The form shows a real-time badge: "Sum: 95 — missing 5".
- Role is free-form informational text (AE, SDR, partner, etc.). Doesn't affect the calculation.
- Complete terms:
- Recurring %: shows the origin of the value in real time (input → rep default → global default).
- Recurring fixed (cents)
- One-time amount (cents)
- Basis
- Start date (
startsAt). Required. - End date (
endsAt). Optional — if left empty, the assignment is indefinitely active. - Notes.
- Save.
Validations:
- At least one of the 3 monetary terms must be > 0 (error
NO_TERMS). - Shares must sum to 100 (validated in DB by DEFERRABLE trigger).
- Cannot have two participants with the same rep in the same assignment.
3. Edit splits in an assignment
Route: /admin/commissions/asignaciones → click on the assignment → edit drawer
To change participants (splits):
- In the "Participants" section of the drawer, edit the shares or roles.
- The live validator shows the current sum.
- Save → the service executes
replaceParticipants: DELETE all + INSERT new within a transaction. The DEFERRABLE trigger validates the sum at COMMIT.
Note: Changing participants on an active assignment does not affect statements already finalized or paid (they are snapshots). Existing drafts for the current period will be recalculated the next time the period is materialized.
4. Configure the manager hierarchy
Route: /admin/commissions/vendedores → click on the rep → edit drawer → "Hierarchy" section
To assign a manager:
- "Manager" field: combobox of other active reps (excludes the rep themselves and their reports to prevent cycles).
- Select the manager → save.
- If the system detects a cycle, it shows an inline error: "This change would create a cycle in the hierarchy."
To configure the override %:
- "Override %" field: percentage this rep earns on the commissions of their reports.
- Value 0 (default) = generates no overrides even if they have reports.
- The override is applied recursively walking up the chain: if A → B → C (rep), and A.overridePercent=5, B.overridePercent=10, then for every $100 in commission from C: B earns $10 and A earns $5 (no compounding).
"Direct reports" section: shows the reps with managerId = this.id, each with a link to their drawer.
5. Generate monthly statement (manual)
Route: /admin/commissions/statements → "Generate statements"
Input: Period in YYYY-MM format (e.g., 2026-04).
Process:
- The service calls
materializeStatementsForPeriod('2026-04'). - The calculator fetches the active assignments in that period.
- For each rep with commission > 0, creates or replaces the statement in
draft. - Returns
{ created: N, replaced: M, skipped: K }.
Behavior:
- If a
draftstatement already exists for the same rep + period: replaces it (deletes line items and recalculates). - If the statement is
finalizedorpaid: skips it (doesn't touch it). - Reps with commission = 0: no statement is generated.
6. Automatic cron
Route: /admin/commissions/configuracion → "Monthly cron" section
Available configuration:
- Day of month (1-28): when it runs each month.
- Hour (0-23 UTC): at what time.
- Auto-finalize: if enabled, the cron also automatically finalizes each generated draft.
- Auto-email on finalize: log-only in the current version (F-CMS-1.5 will connect Resend).
Cron behavior:
- BullMQ repeatable job
commissions-monthlyis registered in the worker. - Each tick re-reads the settings (no server restart needed for changes).
- Calculates the previous month's period and calls
materializeStatementsForPeriod. - If
autoFinalize = true, automatically finalizes each generated draft. - On failure: 3 retries with exponential backoff, then marks the job as failed and notifies Sentry.
7. Finalize a statement
Route: /admin/commissions/statements → click on the statement → drawer → "Finalize" button
Precondition: Status = draft.
The system:
- Shows a summary with line items and totals.
- Asks for confirmation.
- On confirm:
- Changes
status = 'finalized',finalizedAt = now(). - For each
kind='one_time'line item in this statement: setscommission_assignments.oneTimePaidAt = periodStart. Prevents the bonus from being paid again.
- Changes
Warning: Once finalized, it cannot be reverted. If the statement has incorrect data, the correction is made with a compensatory revenue event in the next period.
8. Mark as paid
Route: /admin/commissions/statements → click on the finalized statement → "Mark as paid"
Precondition: Status = finalized.
Fields:
- Payment method (required):
ach,wire,check,paypal,wise,other - Payment reference (optional): transfer number, check number, etc.
- Payment notes (optional): notes for the internal record.
Result: status = 'paid', paidAt = now(), payment fields saved. Immutable.
9. Invite a sales rep to the portal
Route: /admin/commissions/vendedores → click on the rep → drawer → "Portal access" section
If the rep has NO account (userId IS NULL):
- If no pending invitation: "Invite to portal" button.
- If pending: text "Invitation sent, expires on {date}" + "Resend" button.
When inviting:
- A 32-byte hex token (64 chars) is generated.
- Previous pending invitations are invalidated.
- An email is sent with the link:
{PORTAL_URL}/seller/setup?token={token}. - The response includes the
setupUrlfor copy/paste (in case the email doesn't arrive).
If the rep ALREADY has an account (userId IS NOT NULL):
- Badge "Seller with portal access."
- "Revoke access" button → sets
userId = null(doesn't delete the user).
Token valid for 7 days. After use it is one-shot (not reusable).
10. Revoke portal access
Route: /admin/commissions/vendedores → click on rep with account → "Revoke access"
Effect:
commission_reps.userId = null.- The seller's next request to the API fails at
sellerProcedure(can't find the rep linked to the user). - The Better Auth user still exists but has no access to the seller portal.
To re-invite: The rep no longer has userId, so the "Invite to portal" button reappears.
11. Configure % per component
Route: /admin/commissions/asignaciones → click on assignment → edit drawer → "% per component" section (collapsible)
Four optional fields:
- % Base
- % Phones
- % Lines
- % Overage
Rule: If at least one is set, the calculator enters breakdown mode: generates up to 4 recurring_percent line items, one per component with revenue > 0. Components with null % use the assignment's global recurringPercent.
If all are empty, the calculator uses legacy mode: a single line item on total revenue.
12. Edit a manual revenue event
Route: /admin/commissions/ingresos → click on the event → edit drawer
Only for non-Stripe sources: wire, check, manual, other.
Events with source='stripe_invoice' are immutable. Attempting to edit them returns error STRIPE_IMMUTABLE. If there's an error in a Stripe event, the correction is to create a manual compensatory revenue event (positive or negative).
Editable fields:
- Period (
YYYY-MM) - Original amount and currency
- FX rate (auto-calculated but editable)
- Source reference
- Component (
base,phones,lines,overage,other) - Notes
13. View the attribution audit log
Route: /admin/commissions/atribucion
Table with filters:
- Lead ID
- Tenant ID
- Action:
lead_ref_code_set,lead_admin_set,lead_converted,admin_reassigned - Date range
Order: createdAt DESC — most recent changes first.
Read-only. Cannot edit or delete the log.
14. Configure global settings
Route: /admin/commissions/configuracion
Sections:
- Global defaults: recurring %, recurring fixed, one-time amount, basis.
- Stripe fees:
stripeFeePercent(default 2.9) andstripeFeeFixedCents(default 30). Used whenbasis='net'. - Vesting:
oneTimeQualificationMonths(default 3). How many months the customer must maintain their subscription before the bonus is paid. - Monthly cron: day of month, UTC hour, auto-finalize, auto-email (log-only).
The cron preview shows: "Runs on day 1 of each month at 03:00 UTC."
15. MSP staff self-service
Route: /admin/my-commissions
Accessible to any admin_msp user whose user.id is in commission_reps.userId. Does not require the commissions.manage permission.
Shows (read-only):
- Their personal statements.
- Their active assignments.
- "Download PDF" button for statements in any status.
Cannot: create, edit, finalize, or mark anything as paid. Read-only only.
If the user is not linked to any rep, the page shows: "You are not registered as a sales rep."