Skip to content

feat(payments-next): Add eligibility check for free trials#20190

Open
david1alvarez wants to merge 1 commit intomainfrom
PAY-3554
Open

feat(payments-next): Add eligibility check for free trials#20190
david1alvarez wants to merge 1 commit intomainfrom
PAY-3554

Conversation

@david1alvarez
Copy link
Contributor

Because:

  • Free trial eligibility is a complex check that requires data from several locations

This commit:

  • Adds in the free trial repository and manager to handle requests for free trial records
  • Adds an eligibility checking method to the checkout service
  • Adds a free trial firestore collection
  • Adds in configs and tests to support the changes

Closes #PAY-3554

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

Any other information that is important to this pull request.

@david1alvarez david1alvarez requested a review from a team as a code owner March 13, 2026 20:06
@david1alvarez david1alvarez force-pushed the PAY-3554 branch 3 times, most recently from 47629bd to d05fc34 Compare March 16, 2026 21:26

export class FreeTrialConfig {
@IsString()
public readonly collectionName!: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to firestoreCollectionName

return this.firestore.collection(this.config.collectionName);
}

async getRecord(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be removed as a method (the repository handles this type of operation)

return getFreeTrialRecordData(this.collectionRef, uid, freeTrialConfigId);
}

async upsertRecord(uid: string, freeTrialConfigId: string): Promise<void> {
Copy link
Member

@julianpoy julianpoy Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be named something more akin to "recordFreeTrial" or "consumeFreeTrialForUser" or "startFreeTrialForUid". The way this is named currently is more akin to a repository record, rather than a manager method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like "recordFreeTrial" for this.

});
}

async isCooldownElapsed(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named something more akin to isEligible since this doesn't throw when the user has no cooldown at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isEligible I think is not specific enough, as this really only checks against the firestore records and the users free trial eligibility is more complex than that. Maybe getFirestoreEligibility? We could also invert it, and call it usedRecently or isBlockedByCooldown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBlockedByCooldown sounds perfect to me!

Comment on lines +12 to +26
const existingRecord = await db
.where('uid', '==', data.uid)
.where('freeTrialConfigId', '==', data.freeTrialConfigId)
.limit(1)
.get();

if (!existingRecord.empty) {
const doc = existingRecord.docs[0];
await doc.ref.update({ startedAt: data.startedAt });
} else {
await db.add({
uid: data.uid,
freeTrialConfigId: data.freeTrialConfigId,
startedAt: data.startedAt,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This upsert is vulnerable to a race condition between reading the existing record and adding a new record. If two calls for upsertFreeTrialRecord occur in reasonably quick succession (within ~70ms of each other in prod, given our Firestore latency), a duplicate will be created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into it more, it seems like Firestore doesn't really seem to have much in the way of preventative measures here. The main option I see is to plan around there being multiple records, and only serving up the latest record when queried. Firestore should be able to handle the number of records we'd be creating this way.

I'm planning to refactor this into getLatestFreeTrialRecordData and createFreeTrialRecord. Any suggestions for other approaches?

Because:

* Free trial eligibility is a complex check that requires data from several locations

This commit:

* Adds in the free trial repository and manager to handle requests for free trial records
* Adds an eligibility checking method to the checkout service
* Adds a free trial firestore collection
* Adds in configs and tests to support the changes
* Updates a test that was outdated and failing

Closes #PAY-3554
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants