diff --git a/README.md b/README.md index e54cdbd..3794e86 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,18 @@ # Hands On OOP +# Merchant Service Simulation -## Deskripsi Tugas -Buatlah suatu program yang *perlu* mengimplementasikan konsep-konsep OOP, seminimalnya -- Inheritance -- Interface dan/atau abstract class -- Polimorfisme -- Mengimplementasikan (minimal 1) design pattern -- Mengimplementasikan (minimal 1) prinsip SOLID +# Deskripsi Program +Program ini berbasis CLI yang mensimulasikan bagaimana user sebagai customer dari sebuah merchant dapat berinteraksi seperti membeli barang, top up balance, dan menu-menu lain. -Tugas ini sangat membebaskan kalian untuk berkreasi. Konsep-konsep lain yang kalian gunakan dan keunikan program akan sangat dihargai dan dihitung sebagai bonus nilai (jangan lupa tulis di penjelasan program). Silakan berkreasi! :D +# Design Pattern +Creational Pattern (Prototype): pada program ini, terdapat abstract class (pada class `baseUser`) yang digunakan untuk membangun class-class yang lain. -### Beberapa Bonus yang Direkomendasikan -- Stream API dan/atau functional programming -- Multilevel inheritance -- Interactive program (Menerima input dan mengeluarkan output sesuai input) - -## Penjelasan Program -Selain membuat program, kalian perlu menuliskan penjelasan program kalian, dengan seminimalnya berisikan -- Deskripsi program -- Penjelasan design pattern yang dipilih -- Letak implementasi design pattern tersebut -- Alasan pemilihan design pattern tersebut -- Cara menjalankan program -- Versi bahasa dan dependency (jika ada) yang digunakan -- Konsep-konsep lain yang kalian gunakan dan keunikan program (jika ada) - -## Pengumpulan -- Pengumpulan tugas ini mirip dengan tugas sebelumnya. -- Fork ke repository github kalian masing - masing -- Buka repository pada repo yang telah di fork sebelumnya -- Clone repository tersebut -- Buat program sesuai deskripsi di atas di dalam folder dengan format `Nama_Univ` -- Tulis penjelasan program kalian pada file `README.md` di dalam folder tersebut -- Add folder tersebut ke dalam staging -- Letakkan folder tersebut sejajar dengan file README.md -- Setelah itu push kembali ke repository kalian -- Pull request kedalam repository GDSC yang sudah anda fork tadi -- Isi judul pull request dengan "Hands on OOP submission by < Nama kalian >" - -## Bahasa Pemrograman -Tidak semua bahasa pemrograman dapat mengimplementasikan OOP. Untuk tugas ini, ada bahasa yang dapat digunakan dan tidak dapat digunakan. - -### Bahasa yang dapat digunakan -Selain bahasa di bawah ini, kalian dapat me-request bahasa lain, dengan persetujuan tim kurikulum GDSC ITB - -- C++ -- Java -- Kotlin -- Typescript -- Go (Golang) -- C# - -### Bahasa yang tidak dapat digunakan -Karena keterbatasan di bahasa-bahasa berikut, bahasa di bawah ini tidak digunakan di tugas ini -- Python -- PHP -- C - -## Deliverable -- Kumpulkan link github fork repository dan link pull request kalian ke gform yang dibagikan - -## Deadline -- Jumat, 27 Januari 2023, pukul 23.59 +# Cara menjalankan program +```shell +cd src +tsc index.ts +node index.js +``` +# Versi Bahasa +- Typescript 4.8.4 (compatible untuk versi ^3.0.0) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2ab6666 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "OOP", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } + }, + "dependencies": { + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1dac93d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@types/node": "^18.11.18" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3a54a20 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,144 @@ +import { sleep } from "./lib/time_sleep"; +import { inputData } from "./lib/readline"; +import { storeItem, adminCredentials } from "./lib/constant/store"; +import { + User, + Merchant, + Auth, +} from "./lib/user"; +import * as console from "console"; + +const main = async () => { + let running = true; + console.log("Welcome~!"); + + let auth = new Auth(); + let merchant = new Merchant( + adminCredentials.username, + adminCredentials.password, + ); + + while (running) { + console.log("1. Login"); + console.log("2. Sign up"); + console.log("3. Exit") + + let userChoice: any = await inputData("choice"); + let validInput = false; + + switch (userChoice){ + case "1": + // login + await auth.login(); + if (auth.currentUserIndex !== -1) { + let data = auth.getData(); + let currentUserIndex = auth.currentUserIndex; + // found login credentials + console.log(`\nHi ${data[auth.currentUserIndex].username}!`); + + let currentUser = new User( + data[currentUserIndex].username, + data[currentUserIndex].password, + data.length + 1 + ); + + let quitMenu = false; + do { + // Menu + do { + console.log("\nMenu") + console.log("1. Check Basket"); + console.log("2. Check Store"); + console.log("3. Top Up Balance"); + console.log("4. Exit"); + userChoice = await inputData("choice"); + + if (userChoice === "1" || userChoice === "2" || userChoice === "3" || userChoice === "4") { + validInput = true; + } + + } while (!validInput) + validInput = false; + + switch (userChoice) { + case "1": + currentUser.getBasketItem(); + console.log("\nWant to checkout things?"); + console.log("1. Yes"); + console.log("2. No"); + userChoice = await inputData("choice"); + + switch (userChoice){ + case "1": + let chosenItem = await currentUser.checkoutPayment(); + if (chosenItem !== -1) { + merchant.markPayment(currentUser.getUsername(), chosenItem); + } + break; + default: + } + + break; + case "2": + console.log("\nSelect item to buy:"); + storeItem.forEach((item) => { + console.log(`\t${item.item_id}. cost: ${item.cost}`); + }) + userChoice = parseInt(await inputData("choice")) - 1; + if (userChoice > 5) { + console.log("Invalid input!"); + } + else { + currentUser.makePayment( + storeItem[userChoice].cost, + storeItem[userChoice].item_id + ) + merchant.addCustomer( + { + username: currentUser.getUsername(), + item_id: storeItem[userChoice].item_id, + total_cost: storeItem[userChoice].cost, + status: "unpaid", + } + ) + console.log(`Successfully bought item with id ${storeItem[userChoice].item_id}\n`); + } + break; + case "3": + currentUser.topUpBalance(); + break; + case "4": + quitMenu = true; + running = false; + break; + default: + } + + } while (!quitMenu) + + } + else { + console.log("Account not found!"); + } + break; + case "2": + await auth.signUp(); + break; + case "3": + running = false; + break; + default: + } + + } +} + +main().then(() => { + sleep(1000).then(() => { + console.log("See you next time!"); + + }) +}); + +// solved TS2451 +export {}; \ No newline at end of file diff --git a/src/lib/constant/store.ts b/src/lib/constant/store.ts new file mode 100644 index 0000000..d3957a3 --- /dev/null +++ b/src/lib/constant/store.ts @@ -0,0 +1,13 @@ +export const storeItem = [ + {item_id: 1, cost: 20000}, + {item_id: 2, cost: 30000}, + {item_id: 3, cost: 50000}, + {item_id: 4, cost: 100000}, + {item_id: 5, cost: 200000}, +]; + +export const adminCredentials = { + username: "admin", + password: "test12345", +} + diff --git a/src/lib/readline.ts b/src/lib/readline.ts new file mode 100644 index 0000000..6cf0d97 --- /dev/null +++ b/src/lib/readline.ts @@ -0,0 +1,21 @@ +import * as readline from "readline" +import { toCapitallize } from "./string_modifier"; + +/** + * Readline module built in Node + */ +export const stdin = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +/** + * input from console + * @async true + * @param user_data captured string from console + * @returns full string of user input + */ +export const inputData = async (user_data: string) => { + let userInput: string = await new Promise(resolve => stdin.question(`Enter ${user_data.toLowerCase()}: `, resolve)); + return toCapitallize(userInput); +} \ No newline at end of file diff --git a/src/lib/string_modifier.ts b/src/lib/string_modifier.ts new file mode 100644 index 0000000..c74be67 --- /dev/null +++ b/src/lib/string_modifier.ts @@ -0,0 +1,17 @@ +/** + * Takes in full string and return the capitalized version + * @param str input string + * @returns capitalized string + * */ +export const toCapitallize = (str: string) => { + let tempStr: string; + let rawStrArr = str.split(" "); + rawStrArr.forEach((ele, index) => { + tempStr = ele.toLowerCase(); + let strArr = tempStr.split(""); + strArr[0] = strArr[0].toUpperCase(); + rawStrArr[index] = strArr.join(""); + }) + tempStr = rawStrArr.join(" "); + return tempStr; +} \ No newline at end of file diff --git a/src/lib/time_sleep.ts b/src/lib/time_sleep.ts new file mode 100644 index 0000000..5434e77 --- /dev/null +++ b/src/lib/time_sleep.ts @@ -0,0 +1,10 @@ +/** + * Set time sleep for given milliseconds + * @param ms millis + * @returns timeout Promise + */ +export const sleep = async (ms: number) => { + return new Promise (resolve => setTimeout(resolve, ms)); +} + + diff --git a/src/lib/user.ts b/src/lib/user.ts new file mode 100644 index 0000000..41df393 --- /dev/null +++ b/src/lib/user.ts @@ -0,0 +1,337 @@ +import { stdin, inputData } from "./readline"; +import { sleep } from "./time_sleep"; +import { toCapitallize } from "./string_modifier"; +import { + account, + customer, + paymentObject, + authentication, + credential, +} from "../typings"; + + +// solved TS2420: +// @ts-ignore +abstract class baseUser implements account{ + // Class props + protected _username: string = undefined; + protected _password: string | number = undefined; + protected _id: number = undefined; + protected balance: number = 0; + // Status set to paid + protected isMerchant: boolean = false; + protected status: string = "paid"; + + /** + * @param user_data changed user field + * @returns error message for set / unset value + * */ + protected _errChangesMsg ( + user_data: string, + ) { + return `${toCapitallize(user_data)} cannot be same as previous!`; + } + /** + * @param user_data changed user field + * @returns success message for changes on value + * */ + protected _succChangesMsg ( + user_data: string + ) { + return `Successfully changes ${user_data.toLowerCase()}!`; + } + + // Getters + public getUsername () { + return this._username; + } + + public changeUsername (newUsername: string) { + if (this._username === newUsername) { + console.log(this._errChangesMsg("username")); + } + else { + this._username = newUsername; + console.log(this._succChangesMsg("username")); + } + } + + protected getPassword () { + return this._password; + } + + public changePassword (newPassword: number | string) { + if (this.getPassword() === newPassword) { + console.log(this._errChangesMsg("new password")); + } else { + stdin.question( + "Confirm Password: ", + async (input: string | number) => { + // TODO : Refactor this code using the new input function + console.log("Processing"); + await sleep(1000); + if (newPassword === input) { + this._password = newPassword; + console.log(this._succChangesMsg("password")) + } else { + console.log("Confirmed password is not same!"); + } + } + ) + } + } + + /** + * Increase the current user balance + * @param topUpValue top up amount, defaults to 50000 + * */ + public topUpBalance (topUpValue: number = 50000) { + this.balance = this.balance + topUpValue; + console.log(`Top up ${topUpValue}. Current balance: ${this.balance}`); + } + + +} + +export class User extends baseUser{ + private paymentBasket: paymentObject[] = []; + // Constructor + constructor(username: string, password: number | string, id: number) { + super(); + this._username = username; + this._password = password; + this._id = id; + } + + // Getters + public getBalance () { + return this.balance; + } + + public getBasket () { + return this.paymentBasket; + } + + /** + * Show all list of item inside current user basket + * */ + public getBasketItem () { + console.log(`${toCapitallize(this._username)}'s basket:`); + console.log("No.----Item Id----Cost"); + for (let i = 0; i < this.paymentBasket.length; i++) { + console.log(`|${i + 1}|\t|${this.paymentBasket[i].item_id}|\t|${this.paymentBasket[i].cost}|`); + } + if (this.paymentBasket.length === 0) { + console.log("Empty here.."); + } + } + + /** + * Takes in item cost & id and push it to basket + * @param cost item cost + * @param id item id + * */ + public makePayment (cost: number, id: number) { + // per unique item have different object + let item: paymentObject = { + username: this._username, + // unique item id to mark transaction + item_id: id, + cost: cost, + // false status === uncheckPayment, status set to true when the item is checked out + status: "unpaid", + }; + this.paymentBasket.push(item); + console.log("Item is stored on your basket. Please check it!"); + } + + /** + * Prompt input to removed selected items inside basket if sufficient + * */ + public async checkoutPayment() { + console.log(`Balance: ${this.balance}`); + let choosenItem: number; + let success: boolean = false; + if (this.paymentBasket.length !== 0) { + const maxChoice: number = this.paymentBasket.length; + let userChoice: number = parseInt(await inputData("choice"), 10); + let endChoice: boolean = false; + while (!endChoice) { + if (userChoice > maxChoice) { + console.log("Invalid input!"); + userChoice = parseInt(await inputData("choice"), 10); + } else { + endChoice = true; + // Filter out the chosen index + this.paymentBasket = this.paymentBasket.filter((ele, index) => { + if (index === userChoice - 1) { + if (ele.cost > this.balance) { + console.log("Not enough balance!"); + choosenItem = -1; + } + else { + success = true; + choosenItem === ele.item_id; + this.balance = this.balance - ele.cost; + return index !== userChoice - 1; + + } + } else { + return index !== userChoice - 1; + } + }) + if (success) { + console.log("Successfully checked out an item! Current Basket: "); + this.getBasketItem(); + } + } + } + } + else { + console.log("Nothing in basket"); + choosenItem = -1; + } + return choosenItem; + } +} + +export class Merchant extends baseUser { + + // List of this merchant customer + customers: customer[] = []; + constructor(username: string, password: string | number) { + super(); + this._username = username; + this._password = password; + } + + /** + * @param customerData customer data + * */ + public addCustomer(customerData: customer) { + this.customers.push(customerData); + } + + /** + * @param username customer username + * @param item_id item id + * @returns removed given customer username from merchant basket + * */ + private removeCustomer(username: string, item_id: string) { + this.customers = this.customers.filter( + (ele) => { + return ele.item_id !== item_id && ele.username !== username; + } + ) + } + + /** + * Mark item status on customers list based on users checked out items + * @param username customer username + * @param item_id item id + * */ + public markPayment (username: string, item_id: string | number) { + // console.log("----------->" + this.customers.length); + this.customers.forEach((customer) => { + if (customer.username === username && customer.item_id === item_id) { + customer.status = "paid"; + } + }) + } + + /** + * Prompt input for username & item id to match the customers data + * @returns confirmed boolean value + * */ + public async confirmPayment() { + const username: string = await inputData("username"); + const item_id: string = await inputData("item id") + const length: number = this.customers.length; + let confirmed: boolean = false; + this.removeCustomer(username, item_id); + if (this.customers.length === length) { + console.log(`Unable to find ${username} (does not exist).`); + } + else { + confirmed = true; + console.log(`Successfully confirmed ${username}!`); + } + return confirmed; + } + + /** + * Shows the content of customers list + * */ + public getCustomerList () { + console.log(`${toCapitallize(this._username)}'s customers:`); + if (this.customers.length > 0){ + this.customers.forEach((ele) => { + console.log(`Username: ${ele.username}`); + console.log(`\tTotal: ${ele.total_cost}`); + console.log(`\tStatus: ${ele.status}`); + }) + } + else { + console.log("No customer."); + } + } + +} + +// solved TS2420: +// @ts-ignore +export class Auth implements authentication { + private database: credential[] = [ + { + username: "admin", + password: "test12345", + balance: 0, + loggedIn: true, + isMerchant: true, + } + ]; + public currentUserIndex: number = -1; + private username: string; + private password: number | string; + + // Getters + public getData () { + return this.database; + } + + /** + * Prompt an login form + * @param asMerchant (not available) + * */ + public async login (asMerchant: boolean = false) { + const username = await inputData("username"); + const password = await inputData("password"); + console.log("Authenticating..."); + let foundCredential = false; + let isMerchant = asMerchant; + this.database.forEach((credential, index) => { + if (credential.username === username && credential.password === password) { + foundCredential = true; + this.currentUserIndex = index; + } + }) + if (foundCredential) { + this.database[this.currentUserIndex].loggedIn = true; + } + } + + /** + * Prompt a sign-up form + * */ + public async signUp () { + let currentUser: credential = { + username: await inputData("username"), + password: await inputData("password"), + balance: 0, + loggedIn: false, + isMerchant: false + } + this.database.push(currentUser); + console.log(`Account created! Please login!`); + } +} diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts new file mode 100644 index 0000000..7850f2c --- /dev/null +++ b/src/typings/index.d.ts @@ -0,0 +1,62 @@ + +export declare interface account { + _username: string; + _password: string | number; + _id: number; + balance: number; + isMerchant: boolean; + status: string; + /** + * @returns current username user object + * */ + getUsername(): string; + + /** + * @description change current username of user object + * @param newUsername new username + * */ + changeUsername(newUsername: string): void; + + /** + * @returns current user password + * */ + getPassword(): string | number; + + /** + * @description change current password of user object + * @param newPassword new password + * */ + changePassword(newPassword: number | string): void; + + /** + * @returns top up user balance + * */ + topUpBalance(): void; +} + +export declare interface paymentObject { + username: string; + item_id: string | number; + cost: number; + status?: string; +} + +export declare interface customer { + username: string; + item_id: string | number; + total_cost: number; + status: string; // paid or unpaid + appeal?: boolean; +} + +export declare interface authentication { + database: credential[]; +} + +export declare interface credential { + username: string; + password: string | number; + balance: number; + loggedIn?: boolean; + isMerchant: boolean; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..72964aa --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "lib": ["es6"], + "module": "CommonJS", + "types": ["node"], + "removeComments": false, + "experimentalDecorators": true, + "noImplicitOverride": true, + "sourceMap": true, + "moduleDetection": "force", + "moduleResolution": "node", + "outDir": "./dist", + "baseUrl": "." +// "paths": { +// "@/*": ["src/*"], +// "@/lib/*": ["src/lib/*"], +// }, + }, + "include": [ + "src", + "src/**/*.ts", + "src/typings" + ], + "exclude": [ + "node_modules", + "dist", + ], + "typeRoots": [ + "src/typings", + "node_modules/@types", + ] +} \ No newline at end of file