Skip to content

Commit d763a9f

Browse files
Keith Fawcettclaude
andcommitted
feat(network): surface payout holdback on offering listings
Federates the per-campaign holdbackDays out to the OpenPartner Network so creators see the policy on the marketplace listing *before* they apply, not after they're already promoting. The Network's offering.terms is freeform jsonb so no protocol / schema migration was needed — only the wire-shape convention. Both sides now agree on terms.payoutHoldbackDays. * Admin offering create form (NetworkOfferings.tsx): when the bound Campaign has a holdbackDays value, stamp it onto terms.payoutHoldbackDays at publish time. Single source of truth — brand sets it once on the Campaign, every Offering backed by that Campaign reflects it. Existing offerings stay as-is until re-saved. * Admin offering list: shows "pays out Nd after conversion" next to the campaign id so the brand sees what creators will see. * Creator-side OfferingDetail: new "Holdback: N days" chip in the terms row alongside cookie window + payout cadence, with a title= tooltip explaining why (refund window / trial alignment). * Creator-side Discover: "· Nd holdback" appended to the commission-description line so it's visible at a glance on the grid before clicking through. Backfill: existing published offerings won't have the field until the brand re-saves. UI handles undefined gracefully — no chip, no trailing text — so the experience just gradually improves as brands edit their listings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4cad68b commit d763a9f

3 files changed

Lines changed: 24 additions & 3 deletions

File tree

apps/portal/src/pages/admin/NetworkOfferings.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface OfferingTerms {
77
commissionDescription?: string;
88
cookieWindowDays?: number;
99
payoutCadence?: string;
10+
payoutHoldbackDays?: number;
1011
bonuses?: string[];
1112
}
1213

@@ -29,6 +30,7 @@ interface Campaign {
2930
name: string;
3031
destinationUrl: string;
3132
deepLinkAllowedDomains: string | null;
33+
holdbackDays: number | null;
3234
}
3335

3436
export function AdminNetworkOfferings() {
@@ -108,6 +110,9 @@ function OfferingList() {
108110
</div>
109111
<p style={{ color: '#6b7280', fontSize: 13, margin: '4px 0' }}>
110112
{o.terms.commissionDescription} · campaign <code>{o.vendorCampaignId}</code>
113+
{o.terms.payoutHoldbackDays != null && o.terms.payoutHoldbackDays > 0 && (
114+
<> · pays out {o.terms.payoutHoldbackDays}d after conversion</>
115+
)}
111116
</p>
112117
{o.description && <p>{o.description}</p>}
113118
<p style={{ fontSize: 13 }}>
@@ -156,6 +161,11 @@ function CreateOfferingForm() {
156161
terms: {
157162
commissionDescription,
158163
cookieWindowDays: cookieWindowDays === '' ? undefined : Number(cookieWindowDays),
164+
// Inherit payout holdback from the bound Campaign so creators
165+
// see the actual policy on the marketplace listing. Single
166+
// source of truth — the brand sets it once on the Campaign,
167+
// every Offering backed by that Campaign reflects it.
168+
payoutHoldbackDays: selectedCampaign.holdbackDays ?? undefined,
159169
},
160170
published: true,
161171
},

apps/portal/src/pages/creator/CreatorDiscover.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ interface OfferingListItem {
1616
vendorId: string;
1717
vendorName: string;
1818
vendorPartnerCount: number;
19-
terms: { commissionDescription?: string };
19+
terms: { commissionDescription?: string; payoutHoldbackDays?: number };
2020
createdAt: string;
2121
myStatus: MyStatus;
2222
}
@@ -83,7 +83,12 @@ export function CreatorDiscoverPage() {
8383
<Link to={`/creator/offerings/${o.id}`}>{o.title}</Link>
8484
</h3>
8585
{o.terms?.commissionDescription && (
86-
<p style={{ fontSize: 13, color: theme.textMuted, margin: '4px 0' }}>{o.terms.commissionDescription}</p>
86+
<p style={{ fontSize: 13, color: theme.textMuted, margin: '4px 0' }}>
87+
{o.terms.commissionDescription}
88+
{o.terms.payoutHoldbackDays != null && o.terms.payoutHoldbackDays > 0 && (
89+
<> · {o.terms.payoutHoldbackDays}d holdback</>
90+
)}
91+
</p>
8792
)}
8893
{o.description && (
8994
<p style={{ fontSize: 14, color: theme.text, margin: '8px 0' }}>
@@ -116,7 +121,7 @@ interface Recommendation {
116121
description: string | null;
117122
vendorId: string;
118123
vendorName: string;
119-
terms: { commissionDescription?: string };
124+
terms: { commissionDescription?: string; payoutHoldbackDays?: number };
120125
reasons: string[];
121126
}
122127

apps/portal/src/pages/creator/CreatorOfferingDetail.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface Offering {
2828
commissionDescription?: string;
2929
cookieWindowDays?: number;
3030
payoutCadence?: string;
31+
payoutHoldbackDays?: number;
3132
bonuses?: string[];
3233
};
3334
createdAt: string;
@@ -147,6 +148,11 @@ export function CreatorOfferingDetailPage() {
147148
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 12, color: theme.textMuted, fontSize: 13 }}>
148149
{offering.terms.cookieWindowDays != null && <span>Cookie window: {offering.terms.cookieWindowDays} days</span>}
149150
{offering.terms.payoutCadence && <span>Payouts: {offering.terms.payoutCadence}</span>}
151+
{offering.terms.payoutHoldbackDays != null && offering.terms.payoutHoldbackDays > 0 && (
152+
<span title="Time after a customer converts before the brand can approve + pay your commission. Aligns with their refund window or trial.">
153+
Holdback: {offering.terms.payoutHoldbackDays} days
154+
</span>
155+
)}
150156
</div>
151157
</Card>
152158

0 commit comments

Comments
 (0)