* upsert: migrate US/CA PayPal without a payment method when flagged
getPaymentMethod no longer throws for a PayPal billing agreement destined for
Stripe US/CA when the account is flagged ignore_payment_method; instead it
returns null so the customer is migrated without a default payment method
(the send_invoice paymentMethodPending flow). Unflagged accounts still throw.
* finalize: expect the plan-code remap for legacy-plan subscriptions
When a subscription carries legacy_remap=1 metadata, compare the Stripe product's planCode against the recorded target plan code instead of the raw Recurly plan code, so the intentional remap is not misread as drift (the price is preserved, so the amount comparison is unchanged). Clear the legacy_remap metadata during post-migration cleanup.
* finalize: expect the assistant add-on reprice
When a subscription carries assist_addon_repriced=1 metadata, compare the assistant item against the recorded target amount (Stripe minor units) instead of the raw Recurly amount, so the intended reprice is not misread as drift. Clear the reprice metadata during post-migration cleanup.
* create_coupons: create migration coupons by name with amount_off, currency and product scope
Create the pipeline's per-region coupon_specs_<region>.csv coupons by name (idempotent, auto-paginated), supporting amount_off + currency (fixed) or percent_off, duration, and applies_to.products, with no promotion code. Region comes from the existing --region flag. The existing name-based percent-off + promotion-code path is unchanged.
* finalize: schedule the pending change as a phase 2
For a subscription flagged pending_change_scheduled, compare drift against the current Recurly state (not the pending change), then in Step 7 release the toolkit schedule and create a fresh two-phase subscription schedule (current until renewal; pending state from renewal with proration disabled and end_behavior release), clearing the scheduling metadata once the schedule exists.
* finalize: flag address-pending migrations and push the needs_to_update_address trait
When a migrated subscription carries metadata.addressPending, append -address-pending to the finalization status and push needs_to_update_address to customer.io (alongside the existing tax-info-pending handling), so the customer can be driven to add a billing address. The flag stays on the subscription so the customer.updated webhook can enable automatic_tax and clear it once the address resolves.
* webhook: heal address-pending migrations when the tax location resolves
On customer.updated (address/tax change), for a customer whose location is no longer unrecognized_location, enable automatic_tax on their migrated subscriptions still flagged metadata.addressPending and clear the flag. Enabling automatic_tax is rejected by Stripe while the location is still invalid, so the update is a safe idempotent no-op until the address is fixed; the cancellation state is never touched.
* cron: cancel unresolved address-pending subscriptions before they renew untaxed
Backstop for the address-pending cohort: searches the address-pending subscriptions in a region and, for any about to renew (within a configurable lead window) whose customer location is still unrecognized_location, cancels it at period end so it never renews untaxed. Skips subscriptions that are already cancelling and those whose location has resolved (healed by the webhook). Dry-run unless --commit.
* upsert: set tax.validate_location=auto on customer update
Conform PAN-copied customers to the account's default tax-location validation. Uses 'auto' rather than 'immediately' (which throws for country-only addresses and would break the address-pending migration flow).
* finalize-migration: send payment method info to customer.io
Add payment_method (card/paypal/none) and needs_to_add_payment_method
traits to the stripe_migration customer.io payload so the migration
email campaign can personalise per payment method without enumerating
the full cross-join. The payment method is read from the already-fetched
customer/subscription (default_payment_method now expanded on that fetch),
so no extra Stripe request is made. Also report a -payment-method-pending
status suffix in the CSV output.
* upsert: flag customers whose address Stripe cannot resolve
After pushing the Recurly address, read customer.tax.automatic_tax; when it is 'unrecognized_location', record address_unrecognized in the output so the subscription is migrated untaxed (address-pending) instead of failing at the billing-migration toolkit. This is the authoritative, region-agnostic detector (covers US/CA country-only and broken non-US addresses, e.g. India with a postal but no state/city). Adds isCustomerAddressUnrecognized helper + unit tests.
* Close reactivation-without-valid-address loophole
* webhook: detect newly-set address/tax with !== undefined
Review follow-up: the address-pending heal guard used a truthy check, which
would miss an address newly set from empty. Harmonise with _billingChanges,
which already uses !== undefined.
* upsert: set tax.validate_location via customerParams
Review follow-up: move the tax.validate_location='auto' directive into the
assembled customerParams instead of overriding it inline at the update call,
so it also appears in the dry-run output.
* create_coupons: guard migration coupons against ambiguous discount specs
Review follow-up: throw a clear per-coupon error if a migration coupon spec
has neither or both of amount_off/percent_off, catching data-processing
issues instead of silently sending percent_off=NaN.
* Rely on presence of customer.tax
* webhook: cover the address-pending heal path in unit tests
The customer.updated fixtures carried no `tax` hash and the mocked region
client had no `stripe` property, so the one test that reached the heal branch
threw and was swallowed by the caller's error handler. The heal logic had no
real coverage as a result.
Add the missing fixture fields and tests covering the heal itself, the
still-unrecognized early return, the skip rules for unflagged and cancelled
subscriptions, the Stripe rejection path that leaves the flag set, and the
missing-tax guard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GitOrigin-RevId: 8162e3e0f01e136d3aa560c22e98f43e36da4eb5
* [web] add preview payload to track-change notification types
* [doc-updater] pass accepted/rejected change data through to web for previews
* [web] snapshot change preview onto accepted/rejected notifications
* [web] render resolved change previews instead of accepted/rejected count lines
* [web] add unit coverage for accepted/rejected change preview flow
* fix: notification carries multiple previews per accept/reject batch; extract shared group helper
* fix: one preview per accept/reject action, drop adjacency grouping
* fix: move buildResolvedChangePreviews out of the email builder class
* fix: type error, make tests more explicit
* fix: render resolver below preview
* fix: overflow text, alignment
* fix: test error
* fix: resolved count
* [document-updater/web] send sparse change preview instead of full doc lines
* fix: store sparse preview, hydrate at render, drop CE gate
* fix: advance slice cursor past tracked-delete text in sparse preview
* fix: inline buildChangeGroupPreview
* fix: changePreviewBuilder -> sparseChangePreview
* fix: rm change_contributors
* fix: copilot flagged testing issues
* fix: hydrate and truncate change preview at write time
* fix: avoid materializing full doc text when slicing preview window
* fix: update reject preview test to hydrated shape
* fix: accepted delete removing context
* [document-updater/web] split resolved change previews into one per change cluster
* fix: narrow cluster userIds to string[] for typecheck
* feat: updating to use zod schema for previews
---------
Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com>
GitOrigin-RevId: 0740713c715a61a220180f353e497b27da6a5b0f
* load affiliations data when rendering library page
* address Copilot review feedback on PR #36522
- default userAffiliations to [] if getUserFullEmails fails, matching
ProjectListController's error handling, so Library/Trash still render
when the institutions service is unavailable
- stop mutating the affiliation object when attaching the email
- rename a misleading test title (affiliation is not necessarily confirmed)
- fix sidebar-ds-nav "hides the widget" tests: they were only waiting for
the /api/project fetch to be called, not for the project list state to
settle, so they passed even before the affiliations check ran
GitOrigin-RevId: 7d04ae7f7dc8d1526c2deb8959099f6e9fe9c4ee
* [web] Remove SubscriptionFormatters, format subscription dates in the frontend
Replace the server-formatted nextPaymentDueAt/nextPaymentDueDate/
trialEndsAtFormatted fields with the raw periodEnd date and format in
the React dashboard (same strings as before, English + UTC). Also
consolidate hand-rolled trial checks onto isInFreeTrial /
SubscriptionHelper.isInTrial.
* [web] Remove the now-unused dateformat dependency
* [web] Address review: payment-dates unit test, UTC dates in acceptance test
Add a dedicated unit test asserting the exact formatted strings from
formatPaymentDate/formatPaymentDateTime, and use explicit UTC timestamps
in the SubscriptionDashboard acceptance fixtures instead of
local-timezone Date constructors.
GitOrigin-RevId: 16ac4f1098b14ef9f1478d22cf39c1357ead7dea
* [web] Add cancel-loss-messaging split test
Populate the cancel confirmation step with plan-specific losses when the
cancel-loss-messaging split test is enabled: "Are you sure you want to
cancel?" heading, the subscription end date, and a card listing what the
user loses moving to the Free plan (compile timeout, AI, collaborators,
history), with the lost benefit struck through next to its Free-plan
replacement.
Shown only to non-trial Student/Standard/Pro individual subscribers with
no other retention offer (trial extension or downgrade upsell) — those
keep the existing confirmation.
Copy matches the mockups in the ticket. No new analytics events; the
existing subscription-page-cancel-button-click / subscription-cancelled
funnel is used.
Part of #35765
* [web] Consolidate ActiveSubscription stories, add cancel confirmation story
Replace the 13 fixture-per-story exports with a single Dashboard story
driven by controls (subscription fixture select, flexible licensing,
cancel-loss-messaging variant), using the CSF factories pattern. Add a
CancelConfirmation story that renders the cancel confirmation step
directly so the cancel-loss-messaging variant can be previewed without
clicking through, with a Professional fixture available to see the Pro
loss rows.
* [web] Use withSplitTests for the ActiveSubscription stories
Register cancel-loss-messaging in defaultSplitTestsArgTypes and drive the
stories' variant from the shared withSplitTests decorator (radio control,
SplitTestContext provided from args) instead of hand-rolled meta tags and
SplitTestProvider. Generalize withSplitTests' typing so it composes with
CSF factories (preview.meta) as well as legacy meta objects.
* [web] Fix Storybook indexing of the ActiveSubscription stories
Storybook's static CSF indexer requires preview.meta() to receive an
object literal, so spread withSplitTests' argTypes/decorators into the
meta literal instead of wrapping the whole config in the helper call.
* [web] Compose ActiveSubscription stories from orthogonal controls
- Wrap the stories in #subscription-dashboard-root so the dashboard's
scoped styles (e.g. the cancel-loss strikethrough) apply in Storybook
- Replace the single fixture dropdown with independent controls (group,
plan, period, currency, trial, status, pending plan change, add-ons,
coupons, pending license change), building the subscription from the
fixtures' base + patches
- Guard impossible mixes with argTypes `if` conditions: individual-only
controls hide when group is on, and pendingLicenseChange only shows
for groups
* [web] Add student plan to the ActiveSubscription story controls
The cancel-loss-messaging variant has student-specific rows (AI
Assistant / 10 collaborators), so the plan control needs a student
option. Groups have no student plans, so for group subscriptions the
student choice maps to the Standard group fixture.
* [web] Provide ol-plans in the ActiveSubscription stories
The cancel confirmation's downgrade branch (monthly Standard is
isEligibleForDowngradeUpsell) looks up paid-personal in ol-plans and
spins forever when the meta tag is missing. Provide ol-plans and
ol-groupPlans from the fixtures, matching the test render helper.
GitOrigin-RevId: e8491c8d39ed059e42ea3442e8d70c3ef304b009
* Record a metric for Stripe API request statuses to monitor rate limiting
* Add on() stub to Stripe mock in StripeClient unit tests
* Record the Stripe rate-limit reason for 429 responses
* Add acceptance test for Stripe rate-limit reason metric
GitOrigin-RevId: 1674b389f5a7fee403f980b6e813a3e2df81063c
* Fix negative AI quota countdown after reset period elapses
Backend (`FeatureUsageRateLimiter`, `TokenUsageRateLimiter`): once the
24h period had lapsed, `remainingUsage`/`remainingTokens` reset to the
full allowance but `resetDate` kept pointing at the old `periodStart +
24h` (i.e. in the past). Advance to `now + 24h` in that case.
Frontend (`NewPlanPaywall`, `ErrorGetPremium`): the "wait X hours and Y
minutes" copy computed `restoreTime - now` without clamping, so a past
`restoreTime` produced negative hours/minutes (users saw "wait -118
hours and -35 minutes"). Clamp to 0.
* Address PR review on quota reset fix
- Keep `resetDate` tied to the stored `periodStart + PERIOD` rather
than jumping to `now + PERIOD` when the period has lapsed. The
moving-target variant unnecessarily reinterpreted a stable field
(existing consumers such as `use-ai-access-gate` check whether the
reset date is in the past).
- Clamp `remainingUsage` / `remainingTokens` at 0 so a concurrent
over-usage never leaks a negative count.
- Align the boundary check with `_checkRateLimit` / `checkUsage`
(inclusive `<=`).
- Cover the period boundary and the clamp in unit tests for both
`FeatureUsageRateLimiter` and `WorkbenchRateLimiter`.
* Truncate test fixtures to whole seconds
`resetDate` is serialized with `Date.toString()`, which has second
resolution, so fixtures with a millisecond component failed the
boundary assertions.
* Revert code changes; keep tests that document the bug
The fixes masked the symptom (negative countdown) without addressing
the underlying issue, and it is not fully understood yet. Revert the
rate limiters and the writefull paywall components to their original
behaviour, and instead pin the current behaviour in tests:
- `it.fails` tests document that `remainingUsage`/`remainingTokens`
go negative when usage exceeds the allowance — these flip red when
the bug is fixed, at which point they should become plain `it`.
- The lapsed-period tests assert (with explanatory comments) that
`resetDate` keeps pointing at the end of the lapsed period, which is
the server-side ingredient of the negative-countdown support ticket.
* Reapply quota fixes
Restores the post-review state of the rate limiters (clamp remaining
counts at 0, inclusive period boundary, resetDate tied to periodStart)
and the writefull paywall components (clamp countdown at 0).
Tests intentionally untouched in this commit; the two `it.fails` tests
now need flipping to plain `it` in a follow-up.
* Flip bug-documenting tests to plain assertions
The clamp fixes are reapplied, so the `it.fails` tests now assert the
fixed behaviour directly. Trim the explanatory comments.
* Set resetDate to `Date.now() + PERIOD_IN_MILLISECONDS` instead of the expired/stale date.
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
* Update lapsed-period tests for the fresh resetDate
`resetDate` is now `Date.now() + PERIOD` once the period has lapsed, so
assert an approximately-fresh date instead of the stale period end.
---------
Co-authored-by: Mathias Jakobsen <mathias.jakobsen@overleaf.com>
GitOrigin-RevId: ad39627ab9e5ceccb2a61443b2b7d39c68600594
[web] Add Stripe wallet payments (Apple Pay / Google Pay) in analytics and remove conversion to "card"
GitOrigin-RevId: f6d404d35eb108d7ddaf8ca1b6c2164cb5100c9c
* [editor-core] gracefully handle no-op TextOperation
* [web] avoid sending sync to pdf requests without a ?file
* [validation-tools] log deeper nested violations
* [editor-core] fix round trip of EditNoOperation
* [web] use custom error for sync to pdf w/o file, fix tests
* [validation-tools] include request params in log msg for violation
* Revert "[editor-core] gracefully handle no-op TextOperation"
This reverts commit c1d37261d5db72b37dbf39428c54edcfb5423f41.
* [web] gracefully handle rejected sync to pdf from context menu
GitOrigin-RevId: 21de7dc96b23575b0d67b6eb0c68ed9c2f86292b
* Simplify the new project dropdown
* Simplify the New project dropdown
* removing artefact of rebase
GitOrigin-RevId: e7e4dc48c10f2297abcd7c5b489ff2308bf9e18b
* feat: transitioning optimized images from a compile mode to a project level toggle. We still persist it for the proect and between users, and ensure that draft mode does not interact with it (silently disabled while draft mode is on)
* feat: cleaning up unused var, tests, and locales
* feat: updating copy
GitOrigin-RevId: 8010edfab164980f14125739e481d8b512f6b3df