@josiahsiegel/stripe-list-pagination-previous-attributes
|
| name | stripe-list-pagination-previous-attributes |
| description | | |
Quick Reference
| Field | Semantic |
|---|---|
event.data.previous_attributes |
ONLY the fields that changed; diff the current object against this |
event.data.previous_attributes.amount_refunded |
Previous cumulative refund total — subtract from charge.amount_refunded for per-event delta |
charge.amount_refunded |
CUMULATIVE across all refunds, NEVER per-event |
invoice.lines.has_more |
true -> embedded data is page 1; paginate with starting_after |
invoice.lines.data.at(-1).id |
The cursor for starting_after on the next page |
stripe.invoices.listLineItems(invoice.id, { starting_after }) |
Resumes AFTER the cursor — does NOT re-scan page 1 |
When to Use This Skill
Use whenever:
- You read an embedded
.data[]array from a Stripe object and might need to paginate - You compute a "what changed" delta from a webhook event
- You resolve a plan / SKU / entitlement from invoice line items or subscription items
Related skills:
- Where
getRefundDeltais consumed in the refund handler:stripe-billing-master:stripe-refund-dispute-lifecycle - Where
resolvedViagates email rendering and audit logging:stripe-billing-master:stripe-credit-audit-trail
Pagination scan pattern
async function scanAllLineItems(invoice: Stripe.Invoice): Promise<Stripe.InvoiceLineItem[]> {
const items: Stripe.InvoiceLineItem[] = [...invoice.lines.data];
let cursor = invoice.lines.data.at(-1)?.id;
let hasMore = invoice.lines.has_more;
while (hasMore && cursor) {
const page = await stripe.invoices.listLineItems(invoice.id, {
limit: 100,
starting_after: cursor,
});
items.push(...page.data);
hasMore = page.has_more;
cursor = page.data.at(-1)?.id;
}
return items;
}
Without starting_after, stripe.invoices.listLineItems(invoice.id) re-fetches page 1 — the same page you already have embedded in invoice.lines.data. The scan loops over identical content until has_more never flips, concluding "no match" on lines that never got scanned. Always thread the cursor.
Edge case: if
invoice.lines.datais empty butinvoice.lines.has_moreistrue(rare, but Stripe can return this shape when the embeddedlimitis 0), the loop above never executes becausecursorisundefined. In that case, fall through tostripe.invoices.listLineItems(invoice.id, { limit: 100 })with nostarting_afterto start a fresh scan.
previous_attributes delta helper
type AmountRefundedChanged = { amount_refunded?: number };
export function getRefundDelta(event: Stripe.Event, charge: Stripe.Charge): number | null {
const prev = (event.data.previous_attributes as AmountRefundedChanged | undefined)?.amount_refunded;
if (typeof prev === "number") return charge.amount_refunded - prev;
return null; // caller falls back to embedded / list / skip-revocation
}
Plan resolver with pagination + G6 safety fallback
export async function resolvePlanFromInvoice(invoice: Stripe.Invoice): Promise<{
plan: Plan;
credits: number;
resolvedVia: "priceMap" | "safetyFallback";
}> {
try {
const items = await scanAllLineItems(invoice); // G3 -- paginate first
for (const item of items) {
const mapped = PRICE_TO_PLAN[item.price?.id ?? ""];
if (mapped) return { ...mapped, resolvedVia: "priceMap" };
}
logEvent("credit_price_resolve_unknown", { invoiceId: invoice.id, lineCount: items.length });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
} catch (err) {
logEvent("credit_price_resolve_error", { invoiceId: invoice.id, err: String(err) });
return { plan: "free", credits: 0, resolvedVia: "safetyFallback" }; // G6
}
}
The email renderer uses resolvedVia to gate plan names on priceMap — never renders "Welcome to Free" on the safety-fallback path (G-bonus).
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.