Created
September 22, 2024 14:47
-
-
Save jackhftang/32ff5bfbe07042bd8a15986cd214358d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S deno run --allow-env --allow-ffi | |
import { DB } from "https://deno.land/x/sqlite/mod.ts"; | |
const schema = ` | |
CREATE TABLE IF NOT EXISTS User ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
username TEXT | |
); | |
CREATE TABLE IF NOT EXISTS Wallet ( | |
user_id INTEGER, | |
currency TEXT, | |
balance INT, | |
is_freezed BOOLEAN, | |
PRIMARY KEY (user_id, currency), | |
FOREIGN KEY (user_id) references User(id) | |
); | |
`; | |
// ------------------------------- Exceptions ---------------------------------- | |
class UserNotFoundError extends Error { | |
readonly userId: number; | |
constructor(userId: number) { | |
super(); | |
this.userId = userId; | |
} | |
} | |
class WalletNotFoundError extends Error { | |
readonly user: User; | |
readonly currency: string; | |
constructor(user: User, currency: string) { | |
super(); | |
this.user = user; | |
this.currency = currency; | |
} | |
} | |
class SenderWalletFreezedError extends Error { | |
readonly wallet: Wallet; | |
constructor(wallet: Wallet) { | |
super(); | |
this.wallet = wallet; | |
} | |
} | |
class ReceiverWalletFreezedError extends Error { | |
readonly wallet: Wallet; | |
constructor(wallet: Wallet) { | |
super(); | |
this.wallet = wallet; | |
} | |
} | |
class InsufficientFundError extends Error { | |
readonly wallet: Wallet; | |
readonly demand: number; | |
constructor(wallet: Wallet, demand: number) { | |
super(); | |
this.wallet = wallet; | |
this.demand = demand; | |
} | |
} | |
// --------------------------------- Models ------------------------------------ | |
class User { | |
id: number; | |
username: string; | |
constructor(id: number, username: string) { | |
this.id = id; | |
this.username = username; | |
} | |
} | |
class Wallet { | |
userId: number; | |
currency: string; | |
balance: number; | |
isFreezed: boolean; | |
constructor( | |
userId: number, | |
currency: string, | |
balance: number, | |
isFreezed: boolean, | |
) { | |
this.userId = userId; | |
this.currency = currency; | |
this.balance = balance; | |
this.isFreezed = isFreezed; | |
} | |
} | |
// ----------------------------- DB operations --------------------------------- | |
function getUserById(db: DB, userId: number): User { | |
let rows = db.query("SELECT id, username FROM User WHERE id = ?", [userId]); | |
if (rows.length !== 1) throw new UserNotFoundError(userId); | |
let [[id, username]] = rows; | |
return new User(parseInt(id as string), username as string); | |
} | |
function getWallet(db: DB, user: User, currency: string): Wallet { | |
let rows = db.query( | |
"SELECT balance, is_freezed FROM Wallet WHERE user_id = ? and currency = ?", | |
[user.id, currency], | |
); | |
if (rows.length !== 1) throw new WalletNotFoundError(user, currency); | |
let [[balance, isFreezed]] = rows; | |
return new Wallet( | |
user.id, | |
currency, | |
parseInt(balance as string), | |
parseInt(isFreezed as string) != 0, | |
); | |
} | |
function transferMoney( | |
db: DB, | |
a: User, | |
b: User, | |
amount: number, | |
currency: string, | |
) { | |
let wallet1 = getWallet(db, a, currency); | |
if (wallet1.isFreezed) throw new SenderWalletFreezedError(wallet1); | |
let wallet2 = getWallet(db, b, currency); | |
if (wallet2.isFreezed) throw new ReceiverWalletFreezedError(wallet2); | |
if (wallet1.balance < amount) { | |
// check if sender wallet has enough fund to transfer | |
throw new InsufficientFundError(wallet1, amount); | |
} | |
db.query( | |
"UPDATE Wallet SET balance = balance - ? where user_id = ? and currency = ?", | |
[amount, a.id, currency], | |
); | |
db.query( | |
"UPDATE Wallet SET balance = balance + ? where user_id = ? and currency = ?", | |
[amount, b.id, currency], | |
); | |
} | |
// -------------------------------- Handlers ----------------------------------- | |
function handler(db: DB, req: any): string { | |
// todo: validate req here | |
let sender = req.sender as number; | |
let receiver = req.receiver as number; | |
let amount = req.amount as number; | |
let currency = req.currency as string; | |
try { | |
if (sender === receiver) return "Sender cannot same as receiver"; | |
transaction(db, (db) => { | |
let u1 = getUserById(db, sender); | |
let u2 = getUserById(db, receiver); | |
transferMoney(db, u1, u2, amount, currency); | |
}); | |
} catch (ex) { | |
if (ex instanceof UserNotFoundError) { | |
return `Cannot find user with id=${ex.userId}`; | |
} else if (ex instanceof WalletNotFoundError) { | |
return `User ${ex.user.username} do not have ${ex.currency} account`; | |
} else if (ex instanceof SenderWalletFreezedError) { | |
return `Sender account is freezed`; | |
} else if (ex instanceof ReceiverWalletFreezedError) { | |
return `Receiver account is freezed`; | |
} else if (ex instanceof InsufficientFundError) { | |
return `Sender has less than ${ex.demand} in balance`; | |
} else { | |
throw ex; | |
} | |
} | |
return "done"; | |
} | |
// ---------------------------------- Mise ------------------------------------- | |
function transaction(db: DB, f: (db: DB) => void) { | |
try { | |
db.query("BEGIN"); | |
f(db); | |
db.query("COMMIT"); | |
} catch (ex) { | |
db.query("ROLLBACK"); | |
throw ex; | |
} | |
} | |
function prepareFixture(db: DB) { | |
db.execute(schema); | |
let users = ["Peter", "Jack", "Mary"]; | |
for (let name of users) { | |
db.query("INSERT INTO User (username) VALUES (?)", [ | |
name, | |
]); | |
} | |
let wallets = [ | |
[1, "HKD", 1000, false], | |
[1, "USD", 1000, false], | |
[1, "JPY", 1000, false], | |
[2, "HKD", 0, false], | |
[2, "USD", 0, false], | |
[3, "HKD", 100, true], | |
]; | |
for (let wallet of wallets) { | |
db.query( | |
"INSERT INTO Wallet (user_id, currency, balance, is_freezed) values (?, ?, ?, ?)", | |
wallet, | |
); | |
} | |
} | |
// ---------------------------------- main ------------------------------------- | |
function main() { | |
const db: DB = new DB(":memory:"); | |
prepareFixture(db); | |
// assume I have following incoming requests | |
let requests = [ | |
{ sender: 1, receiver: 2, amount: 100, currency: "HKD" }, | |
{ sender: 1, receiver: 2, amount: 1000, currency: "HKD" }, | |
{ sender: 1, receiver: 2, amount: 1000, currency: "JPY" }, | |
{ sender: 1, receiver: 3, amount: 100, currency: "HKD" }, | |
{ sender: 3, receiver: 1, amount: 100, currency: "HKD" }, | |
{ sender: 1, receiver: 4, amount: 100, currency: "HKD" }, | |
]; | |
// process requests | |
for (let req of requests) { | |
console.log("----"); | |
console.log("in: ", req); | |
try { | |
let response = handler(db, req); | |
console.log(response); | |
} catch (ex) { | |
// Exception that is supposed to be caught, | |
// but not caught is considered as logical error | |
// steam exception to log | |
console.error(ex); | |
console.log("internal server error"); | |
} | |
} | |
db.close(); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment