diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b06ab..eda31c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/saturday/types.py b/saturday/types.py index e805027..e5850f4 100644 --- a/saturday/types.py +++ b/saturday/types.py @@ -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 @@ -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 @@ -437,6 +453,10 @@ 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 @@ -444,6 +464,13 @@ class _CoachChargeBreakdownRequired(TypedDict): 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 @@ -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): diff --git a/tests/fixtures/contracts.json b/tests/fixtures/contracts.json index 7d9b1e7..13f248c 100644 --- a/tests/fixtures/contracts.json +++ b/tests/fixtures/contracts.json @@ -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": { @@ -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, @@ -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" } ] }, @@ -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, @@ -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, @@ -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": [ @@ -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": [], diff --git a/tests/fixtures/generate.go b/tests/fixtures/generate.go index 16e41ec..9f7bc4a 100644 --- a/tests/fixtures/generate.go +++ b/tests/fixtures/generate.go @@ -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{ @@ -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}, }