Skip to content

Instantly share code, notes, and snippets.

@jackhftang
Created September 22, 2024 14:47
Show Gist options
  • Save jackhftang/32ff5bfbe07042bd8a15986cd214358d to your computer and use it in GitHub Desktop.
Save jackhftang/32ff5bfbe07042bd8a15986cd214358d to your computer and use it in GitHub Desktop.
#!/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