Set up cloud function for writing txns
This commit is contained in:
parent
7c50c98bc5
commit
ec45dfa311
31
common/txn.ts
Normal file
31
common/txn.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||
export type Txn = {
|
||||
id: string
|
||||
createdTime: number
|
||||
|
||||
fromId: string
|
||||
fromName: string
|
||||
fromUsername: string
|
||||
fromAvatarUrl?: string
|
||||
|
||||
toId: string
|
||||
toName: string
|
||||
toUsername: string
|
||||
toAvatarUrl?: string
|
||||
|
||||
amount: number
|
||||
|
||||
category: TxnCategory
|
||||
// Human-readable description
|
||||
description?: string
|
||||
// Structured metadata for different kinds of txns
|
||||
data?: TxnData
|
||||
}
|
||||
|
||||
export type TxnCategory = 'TO_CHARITY' // | 'TIP' | 'BET' | ...
|
||||
export type TxnData = CharityData // | TipData | BetData | ...
|
||||
|
||||
export type CharityData = {
|
||||
// TODO: Could fill this in
|
||||
}
|
|
@ -64,5 +64,9 @@ service cloud.firestore {
|
|||
allow read;
|
||||
allow write: if request.auth.uid == userId;
|
||||
}
|
||||
|
||||
match /txns/{txnId} {
|
||||
allow read;
|
||||
}
|
||||
}
|
||||
}
|
82
functions/src/transact.ts
Normal file
82
functions/src/transact.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { User } from '../../common/user'
|
||||
import { Txn, TxnCategory, TxnData } from '../../common/txn'
|
||||
|
||||
export const transact = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
data: {
|
||||
amount: number
|
||||
toId: string
|
||||
category: TxnCategory
|
||||
description?: string
|
||||
txnData?: TxnData
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const fromId = context?.auth?.uid
|
||||
if (!fromId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const { amount, toId, category, description, txnData } = data
|
||||
|
||||
if (amount <= 0 || isNaN(amount) || !isFinite(amount))
|
||||
return { status: 'error', message: 'Invalid amount' }
|
||||
|
||||
// run as transaction to prevent race conditions
|
||||
return await firestore.runTransaction(async (transaction) => {
|
||||
const fromDoc = firestore.doc(`users/${fromId}`)
|
||||
const fromSnap = await transaction.get(fromDoc)
|
||||
if (!fromSnap.exists) {
|
||||
return { status: 'error', message: 'User not found' }
|
||||
}
|
||||
const fromUser = fromSnap.data() as User
|
||||
|
||||
const toDoc = firestore.doc(`users/${toId}`)
|
||||
const toSnap = await transaction.get(toDoc)
|
||||
if (!toSnap.exists) {
|
||||
return { status: 'error', message: 'User not found' }
|
||||
}
|
||||
const toUser = toSnap.data() as User
|
||||
|
||||
if (fromUser.balance < amount) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Insufficient balance: ${fromUser.username} needed ${amount} but only had ${fromUser.balance} `,
|
||||
}
|
||||
}
|
||||
|
||||
const newTxnDoc = firestore.collection(`txns/`).doc()
|
||||
|
||||
const txn: Txn = {
|
||||
id: newTxnDoc.id,
|
||||
// @ts-ignore - this is a firestore doc
|
||||
createdTime: admin.firestore.FieldValue.serverTimestamp(),
|
||||
|
||||
fromId,
|
||||
fromName: fromUser.name,
|
||||
fromUsername: fromUser.username,
|
||||
fromAvatarUrl: fromUser.avatarUrl,
|
||||
|
||||
toId,
|
||||
toName: toUser.name,
|
||||
toUsername: toUser.username,
|
||||
toAvatarUrl: toUser.avatarUrl,
|
||||
|
||||
amount,
|
||||
|
||||
category,
|
||||
description,
|
||||
data: txnData,
|
||||
}
|
||||
|
||||
transaction.create(newTxnDoc, txn)
|
||||
transaction.update(fromDoc, { balance: fromUser.balance - amount })
|
||||
transaction.update(toDoc, { balance: toUser.balance + amount })
|
||||
|
||||
return { status: 'success', txnId: newTxnDoc.id }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const firestore = admin.firestore()
|
Loading…
Reference in New Issue
Block a user