Backend (Convex)
API — duitRaya
Generated catalog of Convex functions in convex/duitRaya/. Regenerated by `bun run docs:api`.
Auto-generated from source. Do not edit by hand — run
bun run docs:api.
query (9)
| Function | Description | Args |
|---|---|---|
dashboardGetUserSeasonalClaims | Dashboard query — list a specific user's seasonal campaign claims. Generic across all seasonal campaigns (Raya, CNY, Merdeka, etc.) — the duitRayaCampaigns / duitRayaClaimCodes tables hold every seasonal claim regardless of theme; the legacy table name does not constrain scope. Mirrors getUserClaimsInternal but is callable from the dashboard with an arbitrary userId (admin viewing another user). The mobile-side getUserClaims query continues to use auth context and is unaffected. Auth: requires dashboard access (admin/super_admin/dashboard role). | { userId: v.string(), campaignId: v.optional(v.id("duitRayaCampaigns")), limit: v.optional(v.number()), } |
exportCodes | — | { campaignId: v.id("duitRayaCampaigns"), format: v.optional(v.union(v.literal("csv"), v.literal("json"))), includeClaimedOnly: v.optional(v.boolean()), inclu… |
getActiveCampaigns | Get all active campaigns visible to users OPTIMIZED: Uses aggregate for O(log n) counts | {} |
getAllCampaigns | Get all campaigns for admin dashboard OPTIMIZED: Uses prefix-based queries to handle large datasets | { includeInactive: v.optional(v.boolean()), status: v.optional( v.union(v.literal("all"), v.literal("active"), v.literal("inactive")), ), search: v.optional(… |
getCampaignById | Get campaign by ID OPTIMIZED: Uses aggregate for O(log n) counts instead of scanning 25K+ codes | { campaignId: v.id("duitRayaCampaigns"), } |
getCampaignStats | Get campaign statistics for admin dashboard OPTIMIZED: Queries by prefix to stay under 32K document limit | { campaignId: v.id("duitRayaCampaigns"), } |
getCodes | Get claim codes for a campaign (admin) Returns paginated list of codes with their status OPTIMIZED: Batch fetches user profiles instead of N+1 queries | { campaignId: v.id("duitRayaCampaigns"), status: v.optional( v.union(v.literal("all"), v.literal("unclaimed"), v.literal("claimed")), ), codeType: v.optional… |
getGenerationJobs | Get all generation jobs for a campaign Returns list of jobs with template names (optimized with batch fetch) | { campaignId: v.id("duitRayaCampaigns"), } |
getUserClaims | Get current user's claims Returns all vouchers claimed by the authenticated user | { campaignId: v.optional(v.id("duitRayaCampaigns")), limit: v.optional(v.number()), } |
mutation (8)
| Function | Description | Args |
|---|---|---|
claimCode | Claim a voucher using a QR code Public mutation called when a user scans a QR code. Verifies auth, then delegates to shared processClaim helper. | { code: v.string(), } |
createCampaign | Create a new campaign | { name: v.string(), slug: v.string(), description: v.optional(v.string()), startsAt: v.number(), endsAt: v.number(), isActive: v.optional(v.boolean()), } |
deleteCampaign | Delete a campaign (soft delete) Marks the campaign as inactive and removes associated data. Note: Claimed vouchers are preserved for user records. | { campaignId: v.id("duitRayaCampaigns"), hardDelete: v.optional(v.boolean()), // Only for super_admin } |
generateCodes | — | { campaignId: v.id("duitRayaCampaigns"), voucherTemplateId: v.id("voucherTemplates"), quantity: v.number(), codePrefix: v.optional(v.string()), } |
provisionPool | — | { campaignId: v.id("duitRayaCampaigns"), voucherTemplateId: v.id("voucherTemplates"), quantity: v.number(), codePrefix: v.optional(v.string()), } |
resetCode | Reset a claimed code back to "available" status. Reverses all side effects of processClaim: 1. Deletes the voucher 2. Clears claim fields on the code 3. Updates aggregate stats 4. Resets the pool code to "available" | { codeId: v.id("duitRayaClaimCodes"), } |
resumePoolProvisioning | — | { campaignId: v.id("duitRayaCampaigns") } |
updateCampaign | Update an existing campaign | { campaignId: v.id("duitRayaCampaigns"), name: v.optional(v.string()), description: v.optional(v.string()), startsAt: v.optional(v.number()), endsAt: v.optio… |
internalQuery (2)
| Function | Description | Args |
|---|---|---|
campaignCodeVolumeProbe | Per-campaign code counts, straight from the aggregate. Read-only and O(log n), so it is safe to run against production. bunx convex run --prod duitRaya/queries:campaignCodeVolumeProbe '{}' getCampaignStats scans the campaign's codes twice, once per prefix, capped at MAX_QUERY_LIMIT each. Convex reads at most 32,000 documents per function, so a campaign whose two prefixes together exceed that makes the analytics page throw. This says which campaigns are near the line without reading a single code document. | {} |
campaignPrefixCountProbe | Exact code count for ONE campaign+prefix, with a truncation flag. bunx convex run --prod duitRaya/queries:campaignPrefixCountProbe \ '{"campaignId":"<id>","prefix":"RAYA26"}' One prefix per call, so the probe itself stays under the read limit even when the pair would not. truncated true means the real count is higher than count — which is exactly the condition that makes getCampaignStats report a denomination breakdown over only part of the campaign. | { campaignId: v.id("duitRayaCampaigns"), prefix: v.string(), cap: v.optional(v.number()), } |
internalMutation (12)
| Function | Description | Args |
|---|---|---|
addShopifyCodesToPoolInternal | Add Shopify codes to pool (internal) | { campaignId: v.id("duitRayaCampaigns"), voucherTemplateId: v.id("voucherTemplates"), codes: v.array(v.string()), shopifyJobId: v.optional(v.string()), } |
backfillCodesByDenom | Bring codesByDenom up on a deployment whose codeStats is already correct. Usage: bunx convex run duitRaya/mutations:backfillCodesByDenom '{"cursor": null}' clearCodeStats + backfillCodeStats rebuilds BOTH aggregates, which means clearing one that is currently serving traffic: on production the duit raya pages would read zero for the ~25 minutes the rebuild takes. This touches only the new aggregate, so the live counts never move. Use it when shipping codesByDenom to an existing deployment; use the pair above only when codeStats itself is suspect. Batch is 100 here against 50 there, because this writes one aggregate per row rather than two. | { cursor: v.union(v.string(), v.null()), batchSize: v.optional(v.number()), totalProcessed: v.optional(v.number()), } |
backfillCodeStats | Backfill both claim-code aggregates for existing codes. Self-scheduling: processes a batch, then schedules itself for the next batch. Idempotent — safe to re-run (uses insertIfDoesNotExist). Usage: bunx convex run duitRaya/mutations:backfillCodeStats '{"cursor": null}' Runs to completion automatically — no manual cursor passing needed. The batch size is 50, not 200. claimCodeAggregates maintains two aggregates, so each row costs two B-tree insertions, and each of those is several system operations that grow with the depth of the tree. At 200 the chain ran fine for 19,600 rows of the 43,805-row campaign and then died on "Your request timed out performing too many system operations" — late enough to look like a hang rather than a failure, and the chain stops silently because the mutation that would schedule the next batch is the one that failed. Raise this only with a probe run afterwards to prove the aggregate actually finished. | { cursor: v.union(v.string(), v.null()), batchSize: v.optional(v.number()), totalProcessed: v.optional(v.number()), } |
cancelCampaignHardDelete | Operational kill switch for a runaway campaign hard delete. continueCampaignHardDelete re-schedules itself until the campaign is gone, which is correct but unstoppable once started. This cancels every pending continuation so a delete fired at the wrong campaign can be halted partway. Filtered by function name so it cannot touch webhooks, workpools or any other scheduled work. Codes already deleted are NOT restored — this stops the bleeding, it does not undo it. | {} |
clearCodeStats | Clear the codeStats aggregate completely. Run this before re-backfilling if counts are wrong. Usage: bunx convex run duitRaya/mutations:clearCodeStats '{}' | {} |
completeJob | Complete generation job | { jobId: v.id("duitRayaGenerationJobs"), status: v.union(v.literal("completed"), v.literal("failed")), processedCodes: v.number(), failedCodes: v.number(), s… |
continueCampaignHardDelete | Continue a hard delete that outran one transaction. Re-schedules itself until a pass reports done, and stops quietly if the campaign is already gone. | { campaignId: v.id("duitRayaCampaigns") } |
countTableDocs | Count actual documents in duitRayaClaimCodes table (diagnostic). Paginates through the table to get an exact count without scanning all docs. Usage: bunx convex run duitRaya/mutations:countTableDocs '{"cursor": null}' | { cursor: v.union(v.string(), v.null()), runningTotal: v.optional(v.number()), } |
deleteCodeBatch | Delete a batch of claim codes from the database. Used by cleanupExcessCodes action and cleanup-db.ts script. Safety: skips any code that has been claimed (claimedAt !== undefined). Limit: max 10 codes per call to stay within Convex's document read limit (each code does 2 index lookups + aggregate ops ≈ 600 reads). | { codes: v.array(v.string()), // max 10 per call } |
provisionPoolBatch | One batch of the provisioning loop. Re-schedules itself while codes remain; records the failure on the campaign and stops otherwise, so the admin can resume once the cause (quota, Shopify link) is fixed. | { campaignId: v.id("duitRayaCampaigns"), adminUserId: v.string(), adminRole: v.string(), } |
updateJobProgress | Update generation job progress | { jobId: v.id("duitRayaGenerationJobs"), processedCodes: v.number(), failedCodes: v.number(), shopifyJobIds: v.optional(v.array(v.string())), } |
verifyAggregate | Diagnostic: compare table doc count vs aggregate count per campaign. Usage: bunx convex run duitRaya/mutations:verifyAggregate '{}' --prod | {} |
internalAction (3)
| Function | Description | Args |
|---|---|---|
deleteShopifyCodesBatch | Delete a batch of discount codes from Shopify (max 250 IDs per call). Used by the local cleanup-db.ts script to remove excess codes from Shopify after local filtering. | { discountId: v.string(), ids: v.array(v.string()), } |
fetchDiscountCodesPage | Fetch one page of discount codes from Shopify (max 250 per page). Used by the local cleanup-db.ts script to paginate through all codes without hitting Convex's 8,192-item array argument limit. | { discountId: v.string(), cursor: v.optional(v.string()), } |
generateShopifyCodes | Generate Shopify discount codes and add to pool This is an internal action called by the generateCodes mutation. It creates discount codes in Shopify and stores them in the pool. | { jobId: v.id("duitRayaGenerationJobs"), campaignId: v.id("duitRayaCampaigns"), voucherTemplateId: v.id("voucherTemplates"), shopifyDiscountId: v.string(), q… |