Points
What it is
A loyalty-points system that rewards two different behaviors around event memories (photo/video uploads): hosts earn points when guests upload memories to their events, and guests earn points for uploading memories themselves. Points can be converted to cash.
Core concepts
- Two point types,
PointType.HOST_POINTSandPointType.GUEST_POINTS, sharing the samepoint_balances/point_transactionstables but with different earning rules and cash-conversion rates. - Host points: 1 point per unique guest, across all of the host's events — a guest who uploads memories to five of your events still only earns you one point, but a guest uploading to another host's event earns that host a separate point. Enforced by a
(host_id, guest_id)unique constraint inhost_guest_memory_tracking. - Guest points: 1 point per event — a guest uploading 10 memories to the same event earns one point; uploading to a different event earns another. Enforced by a
(guest_id, event_id)unique constraint inguest_event_memory_tracking. - Both are issued asynchronously via Google Pub/Sub, not inline with the upload request — the memory upload endpoint returns immediately, and a subscriber (
HostPointsSubscriberHandler/GuestPointsSubscriberHandler) does the eligibility check and award in the background.
How it works
- Guest uploads a memory:
POST /activity-log/:eventId/upload-memory. ActivityLogServicepublishes to bothpoints.host.issueandpoints.guest.issuePub/Sub topics (non-blocking) — host points skip if guest == host.- Each subscriber independently checks its own uniqueness table, and if the pair is new, creates a tracking row, awards the point, and writes a
point_transactionsrecord. - Guests can then:
GET /v1/points/list— see all their point-type balances at a glance.GET /v1/points/:pointType/details— balance detail for one type.GET /v1/points/:pointType/history— transaction history.POST /v1/points/:pointType/convert-to-cash— cash out, subject to the rules below.
Cash conversion rates
Conversion rate and minimums are per point type, defined in PointsService's pointConfigs:
| Point type | Rate | Minimum conversion |
|---|---|---|
| Host points | 1 point = ₦100 | 10 points (₦1,000) |
| Guest points | 1 point = ₦20 | 100 points (₦2,000) |
Host points are worth 5x more per point — reflects that hosting an event that attracts unique-guest memory uploads is a stronger signal than any individual guest's single upload.
Key business rules & edge cases
- Uniqueness is enforced by a DB constraint, not application logic alone —
(host_id, guest_id)and(guest_id, event_id)are bothUNIQUEindexes, so even a race between two concurrent uploads can't double-award a point. - A conversion request below the type's
minimumConversionis rejected outright, not partially processed — check the current balance and minimum before calling convert-to-cash, or expect a 400. - Since issuance is async over Pub/Sub, there's a delay (typically seconds) between a memory upload succeeding and the point balance updating — don't assume
GET /detailsreflects a memory uploaded moments ago.
Related endpoints
GET /v1/points/list, GET /v1/points/{pointType}/details, GET /v1/points/{pointType}/history, POST /v1/points/{pointType}/convert-to-cash. Not yet in the generated OpenAPI spec — this module hasn't been through the pilot OpenAPI pass.
Related code
src/modules/points/points.route.ts,points.controller.ts,points.service.tssrc/modules/points/host-points-issuance.service.ts,host-points-subscriber.handler.tssrc/modules/points/guest-points-issuance.service.ts,guest-points-subscriber.handler.tssrc/shared/config/pubsub-topics.config.ts— Pub/Sub topic/subscription config (retries, ack deadlines)- Migration:
src/database/migrations/20251009010228_create_points_tables.ts