Update txn type

This commit is contained in:
James Grugett 2022-04-25 22:29:50 -04:00
parent 45cfa9bcc7
commit 7aa7271c9f
3 changed files with 47 additions and 40 deletions

View File

@ -1,4 +1,5 @@
export interface Charity { export interface Charity {
id: string
slug: string // Note, slugs double as charity IDs slug: string // Note, slugs double as charity IDs
name: string name: string
website: string website: string
@ -232,7 +233,11 @@ export const charities: Charity[] = [
blurb: blurb:
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
}, },
].map((charity) => ({ ].map((charity) => {
...charity, const slug = charity.name.toLowerCase().replace(/\s/g, '-')
slug: charity.name.replace(/\s/g, '-'), return {
})) ...charity,
id: slug,
slug,
}
})

View File

@ -5,15 +5,10 @@ export type Txn = {
createdTime: number createdTime: number
fromId: string fromId: string
// TODO: Do we really want to denormalize name/username/avatar here? fromType: 'user' | 'contract' | 'bank_of_manifold'
fromName: string
fromUsername: string
fromAvatarUrl?: string
toId: string toId: string
toName: string toType: 'user' | 'contract' | 'charity' | 'bank_of_manifold'
toUsername: string
toAvatarUrl?: string
amount: number amount: number

View File

@ -2,30 +2,44 @@ import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { User } from '../../common/user' import { User } from '../../common/user'
import { Txn, TxnCategory, TxnData } from '../../common/txn' import { Txn } from '../../common/txn'
import { removeUndefinedProps } from '../../common/util/object'
export const transact = functions.runWith({ minInstances: 1 }).https.onCall( export const transact = functions
async ( .runWith({ minInstances: 1 })
data: { .https.onCall(async (data: Exclude<Txn, 'id' | 'createdTime'>, context) => {
amount: number const userId = context?.auth?.uid
toId: string if (!userId) return { status: 'error', message: 'Not authorized' }
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 const {
amount,
fromType,
fromId,
toId,
toType,
category,
description,
data: txnData,
} = data
if (fromType !== 'user')
return {
status: 'error',
message: "From type is only implemented for type 'user'.",
}
if (fromId !== userId)
return {
status: 'error',
message: 'Must be authenticated with userId equal to specified fromId.',
}
if (amount <= 0 || isNaN(amount) || !isFinite(amount)) if (amount <= 0 || isNaN(amount) || !isFinite(amount))
return { status: 'error', message: 'Invalid amount' } return { status: 'error', message: 'Invalid amount' }
// run as transaction to prevent race conditions // Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
const fromDoc = firestore.doc(`users/${fromId}`) const fromDoc = firestore.doc(`users/${userId}`)
const fromSnap = await transaction.get(fromDoc) const fromSnap = await transaction.get(fromDoc)
if (!fromSnap.exists) { if (!fromSnap.exists) {
return { status: 'error', message: 'User not found' } return { status: 'error', message: 'User not found' }
@ -48,26 +62,20 @@ export const transact = functions.runWith({ minInstances: 1 }).https.onCall(
const newTxnDoc = firestore.collection(`txns/`).doc() const newTxnDoc = firestore.collection(`txns/`).doc()
const txn: Txn = { const txn: Txn = removeUndefinedProps({
id: newTxnDoc.id, id: newTxnDoc.id,
createdTime: Date.now(), createdTime: Date.now(),
fromId, fromId,
fromName: fromUser.name, fromType,
fromUsername: fromUser.username,
fromAvatarUrl: fromUser.avatarUrl,
toId, toId,
toName: toUser.name, toType,
toUsername: toUser.username,
toAvatarUrl: toUser.avatarUrl,
amount, amount,
category, category,
description, description,
data: txnData, data: txnData,
} })
transaction.create(newTxnDoc, txn) transaction.create(newTxnDoc, txn)
transaction.update(fromDoc, { balance: fromUser.balance - amount }) transaction.update(fromDoc, { balance: fromUser.balance - amount })
@ -75,7 +83,6 @@ export const transact = functions.runWith({ minInstances: 1 }).https.onCall(
return { status: 'success', txnId: newTxnDoc.id } return { status: 'success', txnId: newTxnDoc.id }
}) })
} })
)
const firestore = admin.firestore() const firestore = admin.firestore()