Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- `CoachConnectCharge` gains the optional `stripe_fee_paid_by` (`coach` or `saturday`) and `platform_fee_returned_cents` the transactions endpoint returns. `CoachChargeBreakdown` gains `stripe_fee_paid_by`, `refunded_cents`, `platform_fee_returned_cents`, `disputes_cents` and `dispute_costs_recovered_cents`, and `CoachEarningsSummary` their `total_` sums, which the earnings endpoint returns. `CoachConnectSummary` gains the fee terms the summary endpoint returns: `platform_fee_minimum_cents`, `coach_pays_stripe_fees_from` and Stripe's rates (`stripe_fee_domestic_bps`, `stripe_fee_international_bps`, `stripe_fee_fixed_cents`).
- `CoachSeatState` gains `pending_coach_paid_count`: coach-paid athletes whose seat starts billing when the time they paid for on their own Monthly plan runs out. They are not in `coach_paid_count` until then.

## 0.7.0
Expand Down
36 changes: 35 additions & 1 deletion saturday/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,10 @@ class CoachConnectAccount(_CoachConnectAccountRequired, total=False):


class CoachConnectSummary(TypedDict):
"""connect_account is None for a coach with no Connect account."""
"""connect_account is None for a coach with no Connect account. platform_fee_minimum_cents is the
least Saturday's fee takes from a charge. coach_pays_stripe_fees_from is when the coach starts
paying Stripe's processing fee (Unix ms, 0 while Saturday pays it); the stripe_fee_* fields are
that fee on Saturday's account: a domestic and an international card rate, plus a fixed amount."""

connect_account: Optional[CoachConnectAccount]
is_onboarded: bool
Expand All @@ -418,13 +421,26 @@ class CoachConnectSummary(TypedDict):
lifetime_fees_cents: int
lifetime_net_cents: int
platform_fee_bps: int
platform_fee_minimum_cents: int
coach_pays_stripe_fees_from: int
stripe_fee_domestic_bps: int
stripe_fee_international_bps: int
stripe_fee_fixed_cents: int


class CoachEarningsSummary(TypedDict):
"""total_refunded_cents went back to athletes; total_platform_fee_returned_cents is Saturday's fee
given back on refunds; total_disputes_cents was taken for disputes and their fees, less what was
paid back, of which total_dispute_costs_recovered_cents repaid earlier disputes' costs."""

coach_uid: str
total_gross_cents: int
total_stripe_fee_cents: int
total_platform_fee_cents: int
total_refunded_cents: int
total_platform_fee_returned_cents: int
total_disputes_cents: int
total_dispute_costs_recovered_cents: int
total_net_cents: int
charge_count: int
settled_count: int
Expand All @@ -437,13 +453,24 @@ class _CoachChargeBreakdownRequired(TypedDict):
gross_amount_cents: int
stripe_fees_cents: int
platform_fee_cents: int
refunded_cents: int
platform_fee_returned_cents: int
disputes_cents: int
dispute_costs_recovered_cents: int
net_to_coach_cents: int
currency: str
settlement_status: str
occurred_at: int


class CoachChargeBreakdown(_CoachChargeBreakdownRequired, total=False):
"""stripe_fees_cents is the part of Stripe's processing fee the coach pays; stripe_fee_paid_by says
who paid it, absent when no fee was recorded. refunded_cents went back to the athlete,
platform_fee_returned_cents is Saturday's fee given back on refunds, disputes_cents was taken for
disputes and their fees, less what was paid back, of which dispute_costs_recovered_cents repaid
earlier disputes' costs."""

stripe_fee_paid_by: Literal["coach", "saturday"]
athlete_uid: str
athlete_display_name: str

Expand All @@ -470,8 +497,15 @@ class _CoachConnectChargeRequired(TypedDict):


class CoachConnectCharge(_CoachConnectChargeRequired, total=False):
"""status is refunded once any part of the charge is, and refund_amount_cents says how much.
stripe_fees_cents is the part of Stripe's processing fee the coach pays; stripe_fee_paid_by says who
paid it on a destination charge (absent on a direct charge, where the coach did).
platform_fee_returned_cents is Saturday's fee given back on refunds. Absent fields are zero."""

arrangement_id: str
refund_amount_cents: int
stripe_fee_paid_by: Literal["coach", "saturday"]
platform_fee_returned_cents: int


class _CoachConnectChargesPageRequired(TypedDict):
Expand Down
72 changes: 65 additions & 7 deletions tests/fixtures/contracts.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_meta": {
"backend_sha": "21300070c451cfe4a354fbf4826a1e06e703a895",
"backend_sha": "26ee95de5302a26a301ed15d2c5f704d6a32f56d",
"source": "pkg/api Go JSON types, stored-prescription/list/settings HTTP handlers, and the coach billing structs in pkg/purchase, pkg/connect and pkg/coachbilling; synthetic values, no API calls"
},
"activity": {
Expand Down Expand Up @@ -374,7 +374,11 @@
"total_gross_cents": 240000,
"total_stripe_fee_cents": 8400,
"total_platform_fee_cents": 24000,
"total_net_cents": 207600,
"total_refunded_cents": 2000,
"total_platform_fee_returned_cents": 200,
"total_disputes_cents": 1500,
"total_dispute_costs_recovered_cents": 0,
"total_net_cents": 204300,
"charge_count": 48,
"settled_count": 46,
"settling_count": 2,
Expand All @@ -385,13 +389,35 @@
"charge_group_id": "ch_1",
"gross_amount_cents": 5000,
"stripe_fees_cents": 175,
"stripe_fee_paid_by": "coach",
"platform_fee_cents": 500,
"refunded_cents": 0,
"platform_fee_returned_cents": 0,
"disputes_cents": 0,
"dispute_costs_recovered_cents": 0,
"net_to_coach_cents": 4325,
"currency": "usd",
"settlement_status": "settled",
"occurred_at": 1749480000000,
"athlete_uid": "ath_123",
"athlete_display_name": "A. Rider"
},
{
"charge_group_id": "ch_2",
"gross_amount_cents": 10000,
"stripe_fees_cents": 320,
"stripe_fee_paid_by": "coach",
"platform_fee_cents": 1000,
"refunded_cents": 2000,
"platform_fee_returned_cents": 200,
"disputes_cents": 0,
"dispute_costs_recovered_cents": 0,
"net_to_coach_cents": 6880,
"currency": "usd",
"settlement_status": "settled",
"occurred_at": 1749390000000,
"athlete_uid": "ath_123",
"athlete_display_name": "A. Rider"
}
]
},
Expand All @@ -401,6 +427,10 @@
"total_gross_cents": 0,
"total_stripe_fee_cents": 0,
"total_platform_fee_cents": 0,
"total_refunded_cents": 0,
"total_platform_fee_returned_cents": 0,
"total_disputes_cents": 0,
"total_dispute_costs_recovered_cents": 0,
"total_net_cents": 0,
"charge_count": 0,
"settled_count": 0,
Expand Down Expand Up @@ -435,7 +465,12 @@
"lifetime_charges_cents": 240000,
"lifetime_fees_cents": 32400,
"lifetime_net_cents": 207600,
"platform_fee_bps": 1000
"platform_fee_bps": 1000,
"platform_fee_minimum_cents": 0,
"coach_pays_stripe_fees_from": 0,
"stripe_fee_domestic_bps": 0,
"stripe_fee_international_bps": 0,
"stripe_fee_fixed_cents": 0
},
"billing_connect_summary_none": {
"connect_account": null,
Expand All @@ -447,7 +482,12 @@
"lifetime_charges_cents": 0,
"lifetime_fees_cents": 0,
"lifetime_net_cents": 0,
"platform_fee_bps": 1000
"platform_fee_bps": 1000,
"platform_fee_minimum_cents": 0,
"coach_pays_stripe_fees_from": 0,
"stripe_fee_domestic_bps": 0,
"stripe_fee_international_bps": 0,
"stripe_fee_fixed_cents": 0
},
"billing_connect_transactions": {
"charges": [
Expand All @@ -463,11 +503,29 @@
"currency": "usd",
"status": "succeeded",
"captured_at": 1749480000000,
"stripe_webhook_event_id": "evt_1"
"stripe_webhook_event_id": "evt_1",
"stripe_fee_paid_by": "coach"
},
{
"charge_id": "ch_2",
"arrangement_id": "arr_1",
"coach_uid": "coach_abc",
"athlete_uid": "ath_123",
"amount_cents": 10000,
"platform_fee_cents": 1000,
"stripe_fees_cents": 320,
"net_to_coach_cents": 6880,
"currency": "usd",
"status": "refunded",
"refund_amount_cents": 2000,
"captured_at": 1749390000000,
"stripe_webhook_event_id": "evt_2",
"stripe_fee_paid_by": "coach",
"platform_fee_returned_cents": 200
}
],
"total": 1,
"next_cursor": "1749480000000|ch_1"
"total": 2,
"next_cursor": "1749390000000|ch_2"
},
"billing_connect_transactions_end": {
"charges": [],
Expand Down
7 changes: 4 additions & 3 deletions tests/fixtures/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ func main() {
// shape per endpoint, so the SDK types are checked against real serialization.
func billingFixtures() map[string]any {
acct := &connect.ConnectAccount{CoachUID: "coach_abc", StripeAccountID: "acct_1", ChargesEnabled: true, PayoutsEnabled: true, DetailsSubmitted: true, CardPaymentsStatus: "active", TransfersStatus: "active", Country: "US", DefaultCurrency: "usd", Capabilities: map[string]string{"card_payments": "active"}, OnboardedAt: 1749500000000, UpdatedAt: 1749500000000}
charge := connect.ConnectCharge{ChargeID: "ch_1", ArrangementID: "arr_1", CoachUID: "coach_abc", AthleteUID: "ath_123", AmountCents: 5000, PlatformFeeCents: 500, StripeFeesCents: 175, NetToCoachCents: 4325, Currency: "usd", Status: "succeeded", CapturedAt: 1749480000000, StripeWebhookEventID: "evt_1"}
charge := connect.ConnectCharge{ChargeID: "ch_1", ArrangementID: "arr_1", CoachUID: "coach_abc", AthleteUID: "ath_123", AmountCents: 5000, PlatformFeeCents: 500, StripeFeesCents: 175, NetToCoachCents: 4325, Currency: "usd", Status: "succeeded", CapturedAt: 1749480000000, StripeWebhookEventID: "evt_1", StripeFeePaidBy: "coach"}
refunded := connect.ConnectCharge{ChargeID: "ch_2", ArrangementID: "arr_1", CoachUID: "coach_abc", AthleteUID: "ath_123", AmountCents: 10000, PlatformFeeCents: 1000, StripeFeesCents: 320, NetToCoachCents: 6880, Currency: "usd", Status: "refunded", RefundAmountCents: 2000, CapturedAt: 1749390000000, StripeWebhookEventID: "evt_2", StripeFeePaidBy: "coach", PlatformFeeReturnedCents: 200}
arr := connect.BillingArrangement{ArrangementID: "arr_1", CoachUID: "coach_abc", AthleteUID: "ath_123", StripeConnectAcctID: "acct_1", StripeCustomerID: "cus_1", StripeSubscriptionID: "sub_1", BillingMode: "recurring", AmountCents: 5000, Currency: "usd", Interval: "month", TrialDays: 7, RefundPolicy: "prorated", Status: "active", PlatformFeeBPS: 1000, CreatedAt: 1749400000000, ActivatedAt: 1749400000000}
sub := purchase.TierSubscription{SubscriptionID: "ts_1", SubscriberType: "user", SubscriberID: "coach_abc", Tier: "business", Channel: "web_stripe", SourceSKU: "price_1", StripeSubscriptionID: "sub_biz", Status: "active", CurrentPeriodStart: 1748000000000, CurrentPeriodEnd: 1750600000000, AmountCents: 9900, AutoRenew: true, CreatedAt: 1748000000000, UpdatedAt: 1748000000000}
return map[string]any{
Expand All @@ -205,9 +206,9 @@ func billingFixtures() map[string]any {
"billing_tier_status_empty": &coachbilling.TierStatus{Subscriptions: []purchase.TierSubscription{}, Status: &purchase.SubscriptionStatus{}},
"billing_connect_summary": &connect.DashboardSummary{ConnectAccount: acct, IsOnboarded: true, ActiveArrangements: 3, MonthCharges: 15000, MonthFees: 2025, MonthNet: 12975, LifetimeCharges: 240000, LifetimeFees: 32400, LifetimeNet: 207600, PlatformFeeBPS: 1000},
"billing_connect_summary_none": &connect.DashboardSummary{PlatformFeeBPS: 1000},
"billing_connect_earnings": &coachbilling.ConnectEarnings{Summary: &connect.CoachEarningsSummary{CoachUID: "coach_abc", TotalGrossCents: 240000, TotalStripeFeeCents: 8400, TotalPlatformFeeCents: 24000, TotalNetCents: 207600, ChargeCount: 48, SettledCount: 46, SettlingCount: 2, Currency: "usd"}, Breakdowns: []connect.ChargeBreakdown{{ChargeGroupID: "ch_1", GrossAmountCents: 5000, StripeFeesCents: 175, PlatformFeeCents: 500, NetToCoachCents: 4325, Currency: "usd", SettlementStatus: "settled", OccurredAt: 1749480000000, AthleteUID: "ath_123", AthleteDisplayName: "A. Rider"}}},
"billing_connect_earnings": &coachbilling.ConnectEarnings{Summary: &connect.CoachEarningsSummary{CoachUID: "coach_abc", TotalGrossCents: 240000, TotalStripeFeeCents: 8400, TotalPlatformFeeCents: 24000, TotalRefundedCents: 2000, TotalPlatformFeeReturnedCents: 200, TotalDisputesCents: 1500, TotalDisputeRecoveryCents: 0, TotalNetCents: 204300, ChargeCount: 48, SettledCount: 46, SettlingCount: 2, Currency: "usd"}, Breakdowns: []connect.ChargeBreakdown{{ChargeGroupID: "ch_1", GrossAmountCents: 5000, StripeFeesCents: 175, StripeFeePaidBy: "coach", PlatformFeeCents: 500, NetToCoachCents: 4325, Currency: "usd", SettlementStatus: "settled", OccurredAt: 1749480000000, AthleteUID: "ath_123", AthleteDisplayName: "A. Rider"}, {ChargeGroupID: "ch_2", GrossAmountCents: 10000, StripeFeesCents: 320, StripeFeePaidBy: "coach", PlatformFeeCents: 1000, RefundedCents: 2000, PlatformFeeReturnedCents: 200, NetToCoachCents: 6880, Currency: "usd", SettlementStatus: "settled", OccurredAt: 1749390000000, AthleteUID: "ath_123", AthleteDisplayName: "A. Rider"}}},
"billing_connect_earnings_empty": &coachbilling.ConnectEarnings{Summary: &connect.CoachEarningsSummary{CoachUID: "coach_abc", Currency: "usd"}, Breakdowns: []connect.ChargeBreakdown{}},
"billing_connect_transactions": &coachbilling.ChargesPage{Charges: []connect.ConnectCharge{charge}, Total: 1, NextCursor: "1749480000000|ch_1"},
"billing_connect_transactions": &coachbilling.ChargesPage{Charges: []connect.ConnectCharge{charge, refunded}, Total: 2, NextCursor: "1749390000000|ch_2"},
"billing_connect_transactions_end": &coachbilling.ChargesPage{Charges: []connect.ConnectCharge{}},
"billing_connect_arrangements": &connect.DashboardArrangements{Arrangements: []connect.BillingArrangement{arr}, Total: 1},
}
Expand Down