manifold/functions/src/claim-manalink.ts

103 lines
3.2 KiB
TypeScript
Raw Normal View History

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { User } from 'common/user'
import { Manalink } from 'common/manalink'
import { runTxn, TxnData } from './transact'
export const claimManalink = functions
.runWith({ minInstances: 1 })
.https.onCall(async (slug: string, context) => {
const userId = context?.auth?.uid
if (!userId) return { status: 'error', message: 'Not authorized' }
// Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => {
// Look up the manalink
const manalinkDoc = firestore.doc(`manalinks/${slug}`)
const manalinkSnap = await transaction.get(manalinkDoc)
if (!manalinkSnap.exists) {
return { status: 'error', message: 'Manalink not found' }
}
const manalink = manalinkSnap.data() as Manalink
const { amount, fromId, claimedUserIds } = manalink
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' }
const fromDoc = firestore.doc(`users/${fromId}`)
const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) {
return { status: 'error', message: `User ${fromId} not found` }
}
const fromUser = fromSnap.data() as User
// Only permit one redemption per user per link
if (claimedUserIds.includes(userId)) {
return {
status: 'error',
message: `${fromUser.name} already redeemed manalink ${slug}`,
}
}
// Disallow expired or maxed out links
if (manalink.expiresTime != null && manalink.expiresTime < Date.now()) {
return {
status: 'error',
message: `Manalink ${slug} expired on ${new Date(
manalink.expiresTime
).toLocaleString()}`,
}
}
if (
manalink.maxUses != null &&
manalink.maxUses <= manalink.claims.length
) {
return {
status: 'error',
message: `Manalink ${slug} has reached its max uses of ${manalink.maxUses}`,
}
}
if (fromUser.balance < amount) {
return {
status: 'error',
message: `Insufficient balance: ${fromUser.name} needed ${amount} for this manalink but only had ${fromUser.balance} `,
}
}
// Actually execute the txn
const data: TxnData = {
fromId,
fromType: 'USER',
toId: userId,
toType: 'USER',
amount,
token: 'M$',
category: 'MANALINK',
description: `Manalink ${slug} claimed: ${amount} from ${fromUser.username} to ${userId}`,
}
const result = await runTxn(transaction, data)
const txnId = result.txn?.id
if (!txnId) {
return { status: 'error', message: result.message }
}
// Update the manalink object with this info
const claim = {
toId: userId,
txnId,
claimedTime: Date.now(),
}
transaction.update(manalinkDoc, {
claimedUserIds: [...claimedUserIds, userId],
claims: [...manalink.claims, claim],
})
return { status: 'success', message: 'Manalink claimed' }
})
})
const firestore = admin.firestore()