#Contractor payments and payouts
Module id: MOD-PAYMENTS
Doc profile: capability
Contractor-facing payment schedules and history on /contractor/payments: This week totals, Previous / Upcoming tabs over sent and company-approved time entries, a shift details popup, and a bounded Stripe account affordance. Programme fence MON-MARKETPLACE blocks Connect onboarding, KYC, live balance fetch, and payouts until answered. Admin payment-schedule editors and paid-status writes stay MOD-ADMIN; this module owns the contractor read surface and the Upcoming payment cycle PostMark outbound contract.
#Purpose
Give signed-in contractors one place to see what they are owed and what was already paid for shifts they tracked, open a row’s shift details, and (when an account id exists) open the Stripe dashboard. An upcoming-cycle email can land them on the Upcoming tab. Money writes and Connect account creation are out of scope here — schedules are read models under RLS; Stripe Connect plugins on the source page are fenced, not ported.
#Scope boundaries
| In scope | Out of scope |
|---|---|
/contractor/payments cabinet body: overview, tabs, payment rows, details popup |
Workspace Header / SideBar chrome (MOD-SHELL) |
Read models on time_entries (← time_tracking1) with contractor-scoped RLS |
Client-authoritative money writes (MON-CLIENT-WRITES / LOG-CLIENT-WRITES) |
profiles.stripe_account_id / stripe_name display + dashboard URL when id present |
Stripe Connect plugin create/login, KYC, writing stripe fields from Connect, live GetDataFromAPI balance (MON-MARKETPLACE) |
| Edge send for Upcoming payment cycle (PostMark) + deep-link landing on Upcoming tab | Admin UI that marks paid and invokes the connector today (MOD-ADMIN callers) |
Optional ?tab=previous_payments / ?tab=upcoming_payment |
Inventing a live rates table (all fields deleted in export) |
PageLoaded role gates (anon → /, admin → admin, company → company shifts) |
Company Payments tab body / admin Payments tab (other modules) |
#Actors
| Actor | Behavior in this module |
|---|---|
| Contractor | Opens payments; switches Previous / Upcoming; reviews This week sum and rows; opens Details; uses Stripe control when allowed. |
| Session client | Anon-key Supabase reads under RLS; never ships PostMark or Stripe secret keys. |
| Cycle notifier (edge) | Service/admin-authorized PostMark template send; failures mapped, not raw upstream bodies. |
| Admin (caller only) | Source admin workflows invoke the PostMark call after marking paid — implement UI stays MOD-ADMIN; this module owns the outbound contract. |
#Capability composition
One contractor cabinet route hosts overview cards, schedule tabs, and the repeating payment list. Stripe control and balance label sit in the title/overview row but Connect and live balance stay fenced. Cycle email is an edge outbound that deep-links back into the same route’s Upcoming tab.
Bubble evidence for page, workflows, option sets, and connector is under Traceability / IR evidence.
#Primary journeys
Journeys are reconstructed from contractor_payment PageLoaded and ButtonClicked workflows, RG search states, and the PostMark call Upcoming payment cycle (for contractor). Connect plugin workflows exist on the Stripe control but are not delivered until MON-MARKETPLACE is answered.
#J1 — Contractor reviews payment schedules
- A signed-in contractor opens
/contractor/payments(optionally with?tab=previous_paymentsor?tab=upcoming_payment). - Signed-out visitors redirect to
/; signed-in admin redirects to admin; signed-in company redirects to company shifts — only contractors remain on the page. - The page shows heading “Payments”, overview “This week” with a
$sum ofpaid_amountfor the contractor’s sent rows whose related shift date falls in the current ISO week, and tabs defaulting to Previous payments. - Previous tab lists sent + company-approved rows with
payment_status = paid; Upcoming lists the same filters withpayment_statusnot equal to paid; empty filter shows “No result found”. - Each row shows Company, Payment (
$× logged hours including minutes/60 × shift hours required), Shift date, and Status (“Paid” or “Unpaid” when Upcoming is selected). - Details opens the shift details popup (Company Name, Job position, Shift date, Total of payment via skill-matched company contractor rate × hours, Hours worked, Job location, feedback or “No feedback”); Close hides and resets the popup.
- Load failures show a user-visible error — not a blank success state. No contractor money writes on this surface.
#J2 — Contractor uses evidenced Stripe account fields
- On
/contractor/payments, the Stripe control shows “View stripe account” whenprofiles.stripe_account_idis non-empty, and “Connect Stripe account” when empty. - When an id is present, click opens
https://dashboard.stripe.com/dashboardin a new tab. - When empty, click must not run Connect plugin actions or persist new stripe fields — show a pending/unavailable affordance until MON-MARKETPLACE is answered.
- “Your balance” label may render; live Stripe balance via connected-account header is out of scope — use empty formatting
$0until the programme unblocks it.
#J3 — Upcoming payment cycle email lands on Upcoming tab
- An authorized admin/service caller invokes the upcoming-payment-cycle edge (source admin workflows mark paid then call PostMark; admin UI remains MOD-ADMIN).
- The edge POSTs PostMark
email/withTemplatewith server-only secrets, TemplateAlias evidenced asuser-invitation-3, To = contractor email, and empty TemplateModel as exported — timeout and idempotency enforced. - Product landing contract: the contractor opens
/contractor/payments?tab=upcoming_paymentand sees the Upcoming schedule from J1 (whether that URL is already inside the PostMark template body is HQ-1). - Missing secrets or PostMark failures fail closed with a mapped error; no token leaks to the client.
#Vertical slices (decompose)
| Item | Product outcome |
|---|---|
| PAY-SCHEDULES | Contractor payments page: This week sum, Previous/Upcoming tabs, payment rows, details popup, role gates, time_entries read RLS. |
| PAY-STRIPE-FIELDS | Stripe control copy + dashboard URL when account id present; profiles stripe columns; Connect/balance fenced. |
| PAY-CYCLE-EMAIL | Edge PostMark upcoming-cycle send + deep-link landing on Upcoming tab. |
#Key UI states
| State | When | User sees |
|---|---|---|
| Previous tab (default) | tab_ = previous_payments |
Paid rows; Status “Paid” |
| Upcoming tab | tab_ = upcoming_payment |
Unpaid rows; Status “Unpaid” |
| Empty list | Filtered search empty | “No result found” |
| Details open | Details clicked | Shift details popup for the row |
| Stripe linked | stripe_account_id non-empty | “View stripe account” |
| Stripe unlinked | stripe_account_id empty | “Connect Stripe account” (Connect flow blocked) |
| Balance empty | No live balance (fence) | “Your balance” with $0 empty formatting |
| Wrong role / signed out | PageLoaded gates | Redirect away from payments |
#Style and fidelity
Cabinet workspace outlet on /contractor/payments. Transcribe visible copy (“Payments”, “This week”, “Previous payments”, “Upcoming payment schedule”, column headers, “Details”, “No result found”, popup labels, Stripe control strings). Stamp data-bubble-id on page and payment-region roots. Preserve RG column hierarchy — do not restyle into a marketing card grid. Shared tokens come from the app-level stylesheet that SHELL-LAYOUT (MOD-SHELL) delivers out of IR styles (ir/slices/styles____part_all.json — 49 styles, 8 color tokens, 1 font token); this cycle's module map has no separate design-system module, so do not defer a token to one. Responsive element conditions at narrow widths stay structural tweaks, not a different product layout.
#Failure modes
| Case | Expected behavior |
|---|---|
| Signed-out visit | Redirect to /; no payment UI. |
| Admin or company visit | Redirect to admin or company shifts respectively. |
| Schedule / This week read failure | Visible error; do not pretend empty success. |
| Empty Previous or Upcoming filter | “No result found”. |
| Details with missing shift relation | Show available fields; do not invent job data. |
| Stripe dashboard open failure | User-visible error; stay on page. |
| Stripe id empty click | No Connect plugin; no stripe field writes; pending affordance. |
| Cycle email missing secret | Fail closed; mapped domain error. |
| PostMark upstream failure | Mapped unavailable error; no raw body to client. |
| Anon read of another contractor’s rows | RLS deny. |
#Needs human clarification
| Id | Question | Why unclear (plain + IR) | Impact |
|---|---|---|---|
| HQ-1 | Should the Upcoming payment cycle email deep-link to /contractor/payments?tab=upcoming_payment, and should TemplateModel gain fields, or does PostMark template user-invitation-3 already encode the destination? |
Plain: Bubble admin call sites send TemplateAlias user-invitation-3 with an empty TemplateModel and only interpolate To (contractor email). The template HTML lives in PostMark, not in the export, so whether contractors already get a payments URL is unknown. Decompose PAY-CYCLE-EMAIL assumes a payments deep link for landing behaviour. IR: external_call:bTNMZ:bTNMt, admin actions bTNNo / bTNNp. |
blocks_implement |
#Traceability
| Product concept | Target (route / component / table) | Bubble evidence |
|---|---|---|
| Contractor payments page | /contractor/payments, ContractorPaymentsPage |
bTIzV0 |
| Payment column layout | G-payment region |
bTIzz0 |
| Heading “Payments” | page title | bTJAH0 |
| This week sum | overview card → time_entries.paid_amount |
bTJAp0, paid___number |
| Your balance label / fence | overview card; no live Connect balance | bTJBb0, bTJBg0, bTJBh0 |
| Stripe control copy | StripeAccountControl |
bTJAS0, element_condition:bTJAS0:1 |
| Open Stripe dashboard | new-tab URL | bTMLD, bTMLL |
| Connect plugins (fenced) | not implemented | bTMFn, bTNKQ0, bTNJn0, bTNJs0, bTNJt0, bTNJx0, bTNJy0 |
| Previous / Upcoming tabs | tab_ + tabs option |
bTJBr0, custom_state:bTJBr0:tab_, bTJBs0, bTJBx0, bTJBt0, bTJBy0, bTJCn0, bTJCo0, tabs |
| Set Previous tab | SetCustomState | bTMLQ, bTMOK |
| Set Upcoming tab | SetCustomState | bTMOL, bTMOR |
| Payment rows RG | PaymentRows |
bTJCQ0, bTJCR0 |
| Row payment amount | logged hours×hours_required | bTJCW0 |
| Empty copy | “No result found” | bTJCi0 |
| Status Paid / Unpaid | row status text | bTJCb0 |
| Details → popup | show + display data | bTJCd0, bTJCh0, bTMOW, bTMOc, bTMOd |
| Details popup | PaymentDetailsPopup |
bTJDB0 |
| Close popup | hide + reset | bTJFc0, bTMOh, bTMOn, bTMOo |
| Signed-out redirect | → index | bTNMB2, bTNMD2 |
| Role redirects | → admin / company shifts | bTNMH2, bTNMJ2, bTNMO2 |
| Payment status enum | status_paid / payment_status |
status_paid, bTMpW, status_option_status_paid |
| Time entry fields | time_entries columns |
time_tracking1, send_boolean, approve_company_boolean, contractor_user, logged__hours__number, logged__minuts__number, notes_company_text |
| Stripe profile fields | profiles.stripe_* |
user, stripe_account_id_text, stripe_name_text |
| Dead rates type | omit table | rates (residual dead) |
| Upcoming cycle PostMark | edge + connector | external_ns:bTNMZ, external_call:bTNMZ:bTNMt |
| Admin invoke sites (caller) | MOD-ADMIN | bTNNo, bTNNp |
| Time entry privacy | RLS on time_entries |
privacy_role:time_tracking1:contractor_, privacy_role:time_tracking1:company_, privacy_role:time_tracking1:admin_, privacy_role:time_tracking1:everyone |
#IR evidence
#Module evidence roots
bTIzV0— pagecontractor_payment(/pages/bTIzW0); sole module evidence root.
Supporting segments: ir/slices/bTIzW0____root.json, user, rates (dead), status_paid, external_ns:bTNMZ.
#Path-key ↔ inventory id aliases
| Inventory id | Export path key |
|---|---|
bTIzV0 |
/pages/bTIzW0 |
external_ns:bTNMZ |
/settings/client_safe/apiconnector2/bTNMZ |
external_call:bTNMZ:bTNMt |
/settings/client_safe/apiconnector2/bTNMZ/calls/bTNMt |
#PageLoaded gates (chased)
bTNMB2/bTNMD2: CurrentUsernot_logged_in→ ChangePage index (bTGYf).bTNMH2: logged in and role ≠ contractor →bTNMJ2admin when role = admin (bTHun0);bTNMO2company when role = company (bTITd).
#Schedule filters (chased)
- Default RG search (
bTJCQ0):contractor_user= CurrentUser,send_booleantrue,approve_company_booleantrue,status_option_status_paidequalspaid. - When
G-sorttab_equalsupcoming_payment: same three constraints with status not equalpaid. - Custom state
tab_defaultprevious_payments(custom_state:bTJBr0:tab_); clicks set Previous (bTMOK) or Upcoming (bTMOR).
#This week sum (chased)
- Group
bTJAp0Searchtime_tracking1where contractor = CurrentUser andsend_booleantrue; filter shift.date week equals current week; sumpaid___number.
#Row payment amount (chased)
- Number group behind payment cell (export path under RG content; display text
bTJCW0):logged__hours__number+ (logged__minuts__number/ 60). - Display text
bTJCW0: that number × related shifthours_required_number(via RG contentbTJCR0), prefixed$.
#Stripe control (chased)
- Default copy “View stripe account”; state when
stripe_account_id_textis empty → “Connect Stripe account” (bTJAS0state 1). bTMLD/bTMLL: when id not empty → OpenURLhttps://dashboard.stripe.com/dashboardnew tab.bTMFn: id empty andapprove1_booleanfalse → Connect plugin action (fenced).bTNJn0: id empty andapprove1_booleantrue → Connect login / MakeChangeCurrentUser / OpenURL chain (fenced).- Balance group
bTJBb0: GetDataFromAPI Stripe withStripe-Accountheader = current user’s account id — do not port under MON-MARKETPLACE; keep label +$0empty formatting from source.
#PostMark upcoming cycle (chased)
- Connector name “Upcoming payment cycle (for contractor)”; POST
https://api.postmarkapp.com/email/withTemplate. - Admin actions
bTNNo/bTNNp: body From fixed, To = parent rowcontractor_user.email, TemplateAliasuser-invitation-3, TemplateModel{}. - No TemplateModel deep-link fields in export — see HQ-1.
#Residual dispositions
rates— every field deleted; payment math uses logged hours ×hours_required(list) and company-user skill rate fields (popup), not the rates type.- Style / public_key / settings tokens — answered structurally; not journey items.
#Privacy & authorization (time_tracking1 → time_entries)
privacy_role:time_tracking1:contractor_— view/search when CurrentUser is the contractor on the row.privacy_role:time_tracking1:company_— view/search via shift ownership chain (company role).privacy_role:time_tracking1:admin_— view/search for admin role.privacy_role:time_tracking1:everyone— no view/search.
This module’s SPA only needs contractor-scoped reads on /contractor/payments. Target: RLS so authenticated contractors read only own contractor_id rows; no contractor update of money fields here. Company/admin writers stay on other modules with server authority (MON-CLIENT-WRITES).