This commit is contained in:
Ven
2026-05-12 03:17:27 +07:00
commit 3ab6f6cc39
60 changed files with 6796 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
PAYMENT_ACCOUNT_NAME="Your Account Name"
KBZPAY_NUMBER="09..."
AYAPAY_NUMBER="09..."
AYAPAY_QR_PATH="/aya-pay-qr.png"
ADMIN_TOKEN="replace-with-a-long-random-token"
TELEGRAM_BOT_TOKEN="123456:ABC..."
TELEGRAM_CHAT_ID="123456789"
ORDER_EXPIRY_SECONDS=600
PORT=8000
+21
View File
@@ -0,0 +1,21 @@
orders.db
orders.db-shm
orders.db-wal
uploads/
backend/database/*.db
backend/database/*.db-shm
backend/database/*.db-wal
backend/uploads/
backend/.env
backend/node_modules/
backend/sessions/
.pycache/
__pycache__/
.DS_Store
node_modules/
notification/node_modules/
notification/.env
notification/logs/
live-checking/.env
live-checking/logs/
live-checking/state/
+107
View File
@@ -0,0 +1,107 @@
# Game Top-Up Automation System
Manual payment flow for KBZPay / AYA Pay:
1. User places an order.
2. The system creates an `ORD...` order ID and adds random digits to the product price.
3. User pays the exact amount and uploads a screenshot.
4. Telegram sends you the order, amount, method, screenshot, and action buttons.
5. You verify the payment inside KBZPay / AYA Pay.
6. Confirm, reject, or mark completed from Telegram or the admin panel.
7. Confirmed orders are added to the automation queue.
## Project Structure
```text
frontend/ Customer and admin HTML/CSS/JS
backend/ Queue, database, uploads, and automation worker foundation
bot/ Telegram bot expansion area
notification/ Email alert system
live-checking/ Health, balance, queue, browser, and session monitoring
logs/ Runtime logs
```
## Run
```bash
python3 server.py
```
Open `http://127.0.0.1:8000`.
## Environment
Set these before running in production:
```bash
export PAYMENT_ACCOUNT_NAME="Your Name"
export KBZPAY_NUMBER="09..."
export AYAPAY_NUMBER="09..."
export AYAPAY_QR_PATH="/aya-pay-qr.png"
export ADMIN_TOKEN="use-a-long-random-token"
export TELEGRAM_BOT_TOKEN="123456:ABC..."
export TELEGRAM_CHAT_ID="123456789"
```
Optional:
```bash
export ORDER_EXPIRY_SECONDS=600
export PORT=8000
```
## Automation Worker
The worker is dry-run by default. It reads confirmed orders from the queue and marks them completed without touching Smile.one until real browser automation is connected.
```bash
cd backend
npm run worker:once
npm run worker
```
Set `AUTOMATION_DRY_RUN=false` in `backend/.env` only after the Smile.one browser automation adapter is implemented and tested.
## Telegram Webhook
Point your Telegram bot webhook to:
```text
https://your-domain.com/api/telegram/webhook
```
The bot buttons use callback data:
- `confirm:ORDER_ID` marks the order `paid`
- `reject:ORDER_ID` marks the order `rejected`
- `complete:ORDER_ID` marks the order `completed`
## Database
The app stores SQLite data at `backend/database/orders.db` with:
- `orders`
- `transaction_logs`
- `order_queue`
- `smile_accounts`
Uploads are stored in `backend/uploads/` and served from `/uploads/...`.
Put the AYA Pay banking QR image at `frontend/aya-pay-qr.png`, or set `AYAPAY_QR_PATH` to the public URL/path for your QR image.
## Live Checking
```bash
cd live-checking
npm run check
npm start
```
Dry-run mode is enabled by default so checks log alerts before real email credentials are configured.
## Important Fraud Rules
- Never trust screenshots alone.
- Always verify inside your KBZPay / AYA Pay wallet app.
- Reject unclear screenshots, edited screenshots, wrong amounts, stale payments, or mismatched payment methods.
- Use the transaction log when reviewing disputes.
+16
View File
@@ -0,0 +1,16 @@
SMARTCOIN_API_BASE=http://127.0.0.1:8000
ADMIN_TOKEN=test-token
AUTOMATION_DRY_RUN=true
AUTOMATION_INTERVAL_SECONDS=20
AUTOMATION_MAX_ATTEMPTS=2
AUTOMATION_DELAY_MS=2500
SMILE_ROUTING_STRATEGY=balance-aware
PUPPETEER_HEADLESS=false
PUPPETEER_SLOW_MO_MS=100
PUPPETEER_SESSION_DIR=./sessions
PUPPETEER_NAVIGATION_TIMEOUT_MS=45000
PUPPETEER_EXECUTABLE_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
SMILE_BASE_URL=https://www.smile.one/
@@ -0,0 +1,105 @@
const fs = require("fs");
const path = require("path");
const { loadEnv } = require("../utils/env");
const logger = require("../utils/logger");
loadEnv();
function boolFromEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined) return fallback;
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}
function numberFromEnv(name, fallback) {
const value = Number(process.env[name]);
return Number.isFinite(value) ? value : fallback;
}
function resolveFromBackend(rawPath) {
if (path.isAbsolute(rawPath)) return rawPath;
return path.join(__dirname, "..", rawPath);
}
function getBrowserConfig(accountEmail = "default") {
const sessionRoot = resolveFromBackend(process.env.PUPPETEER_SESSION_DIR || "./sessions");
const safeAccount = accountEmail.replace(/[^a-z0-9._-]/gi, "_");
return {
headless: boolFromEnv("PUPPETEER_HEADLESS", false),
slowMo: numberFromEnv("PUPPETEER_SLOW_MO_MS", 100),
navigationTimeoutMs: numberFromEnv("PUPPETEER_NAVIGATION_TIMEOUT_MS", 45000),
executablePath:
process.env.PUPPETEER_EXECUTABLE_PATH ||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
userDataDir: path.join(sessionRoot, safeAccount),
baseUrl: process.env.SMILE_BASE_URL || "https://www.smile.one/",
};
}
async function launchBrowser(accountEmail) {
const puppeteer = require("puppeteer-core");
const config = getBrowserConfig(accountEmail);
fs.mkdirSync(config.userDataDir, { recursive: true });
const browser = await puppeteer.launch({
headless: config.headless,
executablePath: config.executablePath,
slowMo: config.slowMo,
userDataDir: config.userDataDir,
defaultViewport: {
width: 1366,
height: 900,
},
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
],
});
const page = await browser.newPage();
page.setDefaultNavigationTimeout(config.navigationTimeoutMs);
page.setDefaultTimeout(config.navigationTimeoutMs);
logger.info("Puppeteer browser launched", {
accountEmail,
headless: config.headless,
userDataDir: config.userDataDir,
});
return {
browser,
page,
config,
};
}
async function smokeCheck() {
const session = await launchBrowser("smoke-check");
try {
await session.page.goto(session.config.baseUrl, {
waitUntil: "domcontentloaded",
});
logger.info("Puppeteer smoke check loaded Smile.one", {
url: session.page.url(),
title: await session.page.title(),
});
} finally {
await session.browser.close();
}
}
if (require.main === module) {
smokeCheck().catch((error) => {
logger.error("Puppeteer smoke check failed", {
error: error.message,
stack: error.stack,
});
process.exit(1);
});
}
module.exports = {
getBrowserConfig,
launchBrowser,
};
@@ -0,0 +1,60 @@
const logger = require("../utils/logger");
const { launchBrowser } = require("./browserSession");
const selectors = require("./smileOneSelectors");
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function purchaseTopup({ order, account, dryRun, delayMs }) {
logger.info("Smile.one automation started", {
orderId: order.order_id,
account: account.email,
dryRun,
});
await delay(delayMs);
if (dryRun) {
logger.info("Smile.one dry-run purchase completed", {
orderId: order.order_id,
game: order.game,
product: order.product,
userGameId: order.user_game_id,
});
return {
ok: true,
dryRun: true,
reference: `DRY-${order.order_id}`,
};
}
validateSelectors();
const session = await launchBrowser(account.email);
try {
await session.page.goto(session.config.baseUrl, {
waitUntil: "domcontentloaded",
});
await delay(delayMs);
throw new Error("Smile.one selectors are configured, but the purchase flow still needs site-specific steps");
} finally {
await session.browser.close();
}
}
function validateSelectors() {
const missing = [];
for (const [groupName, group] of Object.entries(selectors)) {
for (const [name, value] of Object.entries(group)) {
if (!value) missing.push(`${groupName}.${name}`);
}
}
if (missing.length > 0) {
throw new Error(`Smile.one selectors are not configured: ${missing.join(", ")}`);
}
}
module.exports = {
purchaseTopup,
};
@@ -0,0 +1,18 @@
module.exports = {
login: {
emailInput: "",
passwordInput: "",
submitButton: "",
loggedInMarker: "",
},
product: {
gameSearchInput: "",
userIdInput: "",
packageButton: "",
confirmButton: "",
successMarker: "",
},
balance: {
amountText: "",
},
};
+1
View File
@@ -0,0 +1 @@
+42
View File
@@ -0,0 +1,42 @@
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL UNIQUE,
game TEXT NOT NULL,
user_game_id TEXT NOT NULL,
product TEXT NOT NULL,
amount INTEGER NOT NULL,
payment_method TEXT,
transaction_id TEXT,
screenshot TEXT,
status TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS transaction_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
action TEXT NOT NULL,
detail TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS order_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK(status IN ('waiting', 'processing', 'completed', 'failed')),
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS smile_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
cookies TEXT,
balance INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL CHECK(status IN ('active', 'paused', 'failed')),
last_used TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
+936
View File
@@ -0,0 +1,936 @@
{
"name": "smartcoin-automation-backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "smartcoin-automation-backend",
"version": "1.0.0",
"dependencies": {
"puppeteer-core": "^24.8.2"
}
},
"node_modules/@puppeteer/browsers": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.1.tgz",
"integrity": "sha512-zmS4RTK9fbrc++WlAJhxYbfz3IjDeOmkK/CwwbLmk7ydfS9e2CiEeRJHEPvjDVElO/bwXbidwGA37Bsm6LzCnQ==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
"tar-fs": "^3.1.1",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz",
"integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==",
"license": "MIT",
"optional": true,
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/b4a": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
"integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
"integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
"integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
"integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
"integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/basic-ftp": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/chromium-bidi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1608973",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz",
"integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==",
"license": "BSD-3-Clause"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"license": "BSD-2-Clause",
"dependencies": {
"debug": "^4.1.1",
"get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
},
"engines": {
"node": ">= 10.17.0"
},
"optionalDependencies": {
"@types/yauzl": "^2.9.1"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"license": "MIT",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
"integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/puppeteer-core": {
"version": "24.43.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.0.tgz",
"integrity": "sha512-cCRNXsUlhyPoKDz6+TiSpfZpRS3mD6Y1YFKhkdr6ik6TMfuJb7fAtXq9ThUFc4sphxObDk3BuAvdxc1Y6YOnqQ==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.1",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1608973",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.20.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/semver": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
"integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"license": "MIT",
"optional": true
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"license": "MIT",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "smartcoin-automation-backend",
"version": "1.0.0",
"private": true,
"description": "Queue and automation worker foundation for SmartCoin game top-up processing.",
"main": "workers/queueWorker.js",
"scripts": {
"worker": "node workers/queueWorker.js",
"worker:once": "node workers/queueWorker.js once",
"browser:check": "node automation/browserSession.js"
},
"dependencies": {
"puppeteer-core": "^24.8.2"
}
}
+1
View File
@@ -0,0 +1 @@
+34
View File
@@ -0,0 +1,34 @@
function getConfig() {
return {
baseUrl: process.env.SMARTCOIN_API_BASE || "http://127.0.0.1:8000",
adminToken: process.env.ADMIN_TOKEN || "test-token",
};
}
async function request(path, options = {}) {
const config = getConfig();
const separator = path.includes("?") ? "&" : "?";
const url = `${config.baseUrl}${path}${separator}token=${encodeURIComponent(config.adminToken)}`;
let response;
try {
response = await fetch(url, {
headers: {
"Content-Type": "application/json",
...(options.headers || {}),
},
...options,
});
} catch (error) {
const cause = error.cause?.message || error.message;
throw new Error(`Cannot reach SmartCoin API at ${config.baseUrl}: ${cause}`);
}
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || `Request failed with HTTP ${response.status}`);
}
return payload;
}
module.exports = {
request,
};
@@ -0,0 +1,28 @@
function activeAccounts(accounts) {
return accounts.filter((account) => account.status === "active");
}
function selectAccount(accounts, order, strategy = "balance-aware") {
const candidates = activeAccounts(accounts);
if (candidates.length === 0) {
throw new Error("No active Smile.one accounts are available");
}
if (strategy === "round-robin") {
return candidates
.slice()
.sort((left, right) => {
const leftUsed = left.last_used || "";
const rightUsed = right.last_used || "";
return leftUsed.localeCompare(rightUsed);
})[0];
}
return candidates
.filter((account) => Number(account.balance || 0) >= Number(order.amount || 0))
.sort((left, right) => Number(right.balance || 0) - Number(left.balance || 0))[0] || candidates[0];
}
module.exports = {
selectAccount,
};
+32
View File
@@ -0,0 +1,32 @@
const fs = require("fs");
const path = require("path");
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const index = trimmed.indexOf("=");
if (index === -1) continue;
const key = trimmed.slice(0, index).trim();
let value = trimmed.slice(index + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (!process.env[key]) process.env[key] = value;
}
}
function loadEnv() {
loadEnvFile(path.join(__dirname, "..", ".env"));
}
module.exports = {
loadEnv,
};
+31
View File
@@ -0,0 +1,31 @@
const fs = require("fs");
const path = require("path");
const logDir = path.join(__dirname, "..", "..", "logs");
const logFile = path.join(logDir, "automation-worker.log");
function write(level, message, meta = {}) {
fs.mkdirSync(logDir, { recursive: true });
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...meta,
};
const line = JSON.stringify(entry);
fs.appendFileSync(logFile, `${line}\n`);
const printer = level === "error" ? console.error : console.log;
printer(line);
}
module.exports = {
info(message, meta) {
write("info", message, meta);
},
warn(message, meta) {
write("warn", message, meta);
},
error(message, meta) {
write("error", message, meta);
},
};
+141
View File
@@ -0,0 +1,141 @@
const fs = require("fs");
const path = require("path");
const { purchaseTopup } = require("../automation/smileOneAutomation");
const { request } = require("../services/apiClient");
const { selectAccount } = require("../services/smileAccountRouter");
const { loadEnv } = require("../utils/env");
const logger = require("../utils/logger");
loadEnv();
function boolFromEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined) return fallback;
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}
function numberFromEnv(name, fallback) {
const value = Number(process.env[name]);
return Number.isFinite(value) ? value : fallback;
}
function getConfig() {
return {
dryRun: boolFromEnv("AUTOMATION_DRY_RUN", true),
intervalSeconds: numberFromEnv("AUTOMATION_INTERVAL_SECONDS", 20),
maxAttempts: numberFromEnv("AUTOMATION_MAX_ATTEMPTS", 2),
delayMs: numberFromEnv("AUTOMATION_DELAY_MS", 2500),
routingStrategy: process.env.SMILE_ROUTING_STRATEGY || "balance-aware",
queueStateFile: path.join(__dirname, "..", "..", "live-checking", "state", "queue-status.json"),
};
}
function writeQueueState(state) {
fs.mkdirSync(path.dirname(state.file), { recursive: true });
fs.writeFileSync(
state.file,
JSON.stringify(
{
ok: state.ok,
pending: state.pending,
failed: state.failed,
message: state.message,
timestamp: new Date().toISOString(),
},
null,
2
)
);
}
async function markQueue(orderId, action, detail = "") {
return request(`/api/admin/queue/${encodeURIComponent(orderId)}/${action}`, {
method: "POST",
body: JSON.stringify({ detail }),
});
}
async function processNextOrder() {
const config = getConfig();
const [{ queue }, { accounts }] = await Promise.all([
request("/api/admin/queue"),
request("/api/admin/smile-accounts"),
]);
const waiting = queue.filter((item) => item.status === "waiting" && item.attempts < config.maxAttempts);
const failed = queue.filter((item) => item.status === "failed").length;
writeQueueState({
file: config.queueStateFile,
ok: failed === 0,
pending: waiting.length,
failed,
message: failed > 0 ? "One or more automation jobs failed" : "",
});
if (waiting.length === 0) {
logger.info("No waiting automation jobs", { failed });
return { processed: false };
}
const order = waiting[0];
await markQueue(order.order_id, "process", "automation worker picked order");
try {
const account = selectAccount(accounts, order, config.routingStrategy);
const result = await purchaseTopup({
order,
account,
dryRun: config.dryRun,
delayMs: config.delayMs,
});
await markQueue(order.order_id, "complete", result.reference || "automation completed");
logger.info("Automation job completed", {
orderId: order.order_id,
reference: result.reference,
dryRun: result.dryRun,
});
return { processed: true, orderId: order.order_id };
} catch (error) {
await markQueue(order.order_id, "fail", error.message);
logger.error("Automation job failed", {
orderId: order.order_id,
error: error.message,
});
return { processed: false, orderId: order.order_id, error: error.message };
}
}
async function runLoop() {
const config = getConfig();
logger.info("Automation worker started", {
intervalSeconds: config.intervalSeconds,
dryRun: config.dryRun,
});
while (true) {
try {
await processNextOrder();
} catch (error) {
logger.error("Automation worker cycle failed", {
error: error.message,
});
}
await new Promise((resolve) => setTimeout(resolve, config.intervalSeconds * 1000));
}
}
if (require.main === module) {
const once = process.argv[2] === "once";
const runner = once ? processNextOrder : runLoop;
runner().catch((error) => {
logger.error("Automation worker failed", {
error: error.message,
stack: error.stack,
});
process.exit(1);
});
}
module.exports = {
processNextOrder,
};
+10
View File
@@ -0,0 +1,10 @@
# Bot
Telegram payment verification currently lives in the main server webhook.
Future bot work should stay in this folder:
- payment verification callbacks
- process / completed / retry buttons
- admin-only commands
- customer status notifications
@@ -0,0 +1,793 @@
<mxfile host="app.diagrams.net" modified="2026-05-11T00:00:00.000Z" agent="Codex" version="24.8.2" type="device">
<diagram id="1-overall-system-architecture" name="1. Overall System Architecture">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="arch-title" value="&lt;b&gt;Overall System Architecture&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="arch-sub" value="Master diagram: customer order, payment verification, queue automation, monitoring, and notifications." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="arch-customer" value="&lt;b&gt;Customer&lt;/b&gt;&lt;br&gt;Places order&lt;br&gt;Uploads payment proof&lt;br&gt;Tracks status" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="50" y="230" width="170" height="105" as="geometry"/>
</mxCell>
<mxCell id="arch-website" value="&lt;b&gt;Website&lt;/b&gt;&lt;br&gt;Frontend pages&lt;br&gt;Order form&lt;br&gt;Tracking UI" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="280" y="230" width="170" height="105" as="geometry"/>
</mxCell>
<mxCell id="arch-api" value="&lt;b&gt;Backend API&lt;/b&gt;&lt;br&gt;Orders&lt;br&gt;Payments&lt;br&gt;Admin routes" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="510" y="230" width="170" height="105" as="geometry"/>
</mxCell>
<mxCell id="arch-db" value="&lt;b&gt;Database&lt;/b&gt;&lt;br&gt;orders&lt;br&gt;smile_accounts&lt;br&gt;logs&lt;br&gt;order_queue" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="740" y="215" width="170" height="130" as="geometry"/>
</mxCell>
<mxCell id="arch-queue" value="&lt;b&gt;Queue&lt;/b&gt;&lt;br&gt;waiting&lt;br&gt;processing&lt;br&gt;completed&lt;br&gt;failed" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="970" y="230" width="170" height="105" as="geometry"/>
</mxCell>
<mxCell id="arch-worker" value="&lt;b&gt;Puppeteer Worker&lt;/b&gt;&lt;br&gt;Reads queue&lt;br&gt;Runs automation&lt;br&gt;Updates status" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="970" y="430" width="170" height="115" as="geometry"/>
</mxCell>
<mxCell id="arch-smile" value="&lt;b&gt;Smile.one&lt;/b&gt;&lt;br&gt;Login/session&lt;br&gt;Game top-up&lt;br&gt;Purchase result" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="740" y="430" width="170" height="115" as="geometry"/>
</mxCell>
<mxCell id="arch-telegram" value="&lt;b&gt;Telegram Bot&lt;/b&gt;&lt;br&gt;Payment proof&lt;br&gt;Confirm / Reject&lt;br&gt;Admin alerts" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="280" y="430" width="170" height="115" as="geometry"/>
</mxCell>
<mxCell id="arch-email" value="&lt;b&gt;Email System&lt;/b&gt;&lt;br&gt;Low balance&lt;br&gt;Failures&lt;br&gt;Critical errors" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="510" y="620" width="170" height="115" as="geometry"/>
</mxCell>
<mxCell id="arch-admin" value="&lt;b&gt;Admin&lt;/b&gt;&lt;br&gt;Checks wallet&lt;br&gt;Controls orders&lt;br&gt;Retries failures" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="50" y="430" width="170" height="115" as="geometry"/>
</mxCell>
<mxCell id="arch-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-customer" target="arch-website">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-website" target="arch-api">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-api" target="arch-db">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-db" target="arch-queue">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-queue" target="arch-worker">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-worker" target="arch-smile">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e7" value="payment notification" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#c026d3;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#c026d3;" edge="1" parent="1" source="arch-api" target="arch-telegram">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e8" value="admin action" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#c026d3;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#c026d3;" edge="1" parent="1" source="arch-telegram" target="arch-admin">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e9" value="confirm / reject" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="arch-admin" target="arch-api">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e10" value="failure alerts" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#c026d3;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#c026d3;" edge="1" parent="1" source="arch-worker" target="arch-email">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="arch-e11" value="result + balance" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#0891b2;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#0891b2;" edge="1" parent="1" source="arch-smile" target="arch-db">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="2-customer-order-flowchart" name="2. Customer Order Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="order-title" value="&lt;b&gt;Customer Order Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="order-sub" value="How a customer creates an order and receives payment instructions." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="order-start" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="90" y="260" width="120" height="70" as="geometry"/>
</mxCell>
<mxCell id="order-game" value="Select game&lt;br&gt;&lt;b&gt;PUBG UC&lt;/b&gt; or &lt;b&gt;MLBB Diamonds&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="270" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-id" value="Enter game ID&lt;br&gt;and server info if needed" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="500" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-package" value="Select package&lt;br&gt;UC / Diamonds amount" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="730" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-payment" value="Select payment method&lt;br&gt;KBZPay / AYA Pay" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="960" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-api" value="Create order API&lt;br&gt;Validate fields" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="270" y="450" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-amount" value="Generate amount&lt;br&gt;Base price + random digits&lt;br&gt;Example: 10000 + 37" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="500" y="440" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="order-store" value="Store order&lt;br&gt;Status: pending" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="450" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="order-show" value="Show payment instructions&lt;br&gt;Order ID&lt;br&gt;Exact amount&lt;br&gt;Wallet number" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="990" y="440" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="order-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-start" target="order-game">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-game" target="order-id">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-id" target="order-package">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-package" target="order-payment">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-payment" target="order-api">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-api" target="order-amount">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-amount" target="order-store">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="order-e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="order-store" target="order-show">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="3-payment-verification-flowchart" name="3. Payment Verification Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="pay-title" value="&lt;b&gt;Payment Verification Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="pay-sub" value="KBZPay / AYA Pay screenshot verification with Telegram admin control." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="pay-start" value="Customer pays exact amount" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="250" width="160" height="80" as="geometry"/>
</mxCell>
<mxCell id="pay-upload" value="Upload screenshot&lt;br&gt;Transaction ID optional" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="300" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="pay-store-img" value="Store image&lt;br&gt;backend/uploads" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="530" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="pay-status" value="Update order&lt;br&gt;Status: waiting_verification" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="250" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="pay-telegram" value="Send Telegram notification&lt;br&gt;Order + screenshot + buttons" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1010" y="240" width="200" height="110" as="geometry"/>
</mxCell>
<mxCell id="pay-wallet" value="Admin checks wallet app&lt;br&gt;Amount / method / time" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="300" y="470" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="pay-valid" value="Payment valid?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="560" y="460" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="pay-confirm" value="Confirm payment&lt;br&gt;Status: paid&lt;br&gt;Add to queue" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="800" y="450" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="pay-reject" value="Reject payment&lt;br&gt;Status: rejected&lt;br&gt;Customer can contact support" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="800" y="610" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="pay-log" value="Save verification log" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1050" y="520" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="pay-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-start" target="pay-upload">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-upload" target="pay-store-img">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-store-img" target="pay-status">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-status" target="pay-telegram">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-telegram" target="pay-wallet">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-wallet" target="pay-valid">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e7" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="pay-valid" target="pay-confirm">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e8" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="pay-valid" target="pay-reject">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-confirm" target="pay-log">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pay-e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pay-reject" target="pay-log">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="4-queue-processing-flowchart" name="4. Queue Processing Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="queue-title" value="&lt;b&gt;Queue Processing Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="queue-sub" value="Safe sequential processing with retry and manual fallback." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="queue-new" value="Paid order" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="260" width="140" height="70" as="geometry"/>
</mxCell>
<mxCell id="queue-wait" value="Waiting queue&lt;br&gt;status: waiting" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="280" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-worker-check" value="Worker checks queue&lt;br&gt;interval loop" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="520" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-has-order" value="Waiting order found?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="235" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="queue-idle" value="No order&lt;br&gt;sleep and check again" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1010" y="170" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-pick" value="Pick one order&lt;br&gt;lock / mark processing" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1010" y="360" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-process" value="Run Puppeteer automation" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="520" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-success" value="Success?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="520" y="505" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="queue-done" value="Mark completed&lt;br&gt;Save result" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="280" y="440" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-retry" value="Attempts left?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="280" y="610" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="queue-back" value="Retry once&lt;br&gt;Return to waiting" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="520" y="660" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="queue-failed" value="Mark failed&lt;br&gt;Manual fallback&lt;br&gt;Alert admin" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="660" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="queue-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-new" target="queue-wait">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-wait" target="queue-worker-check">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-worker-check" target="queue-has-order">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e4" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-has-order" target="queue-idle">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e5" value="loop" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-idle" target="queue-worker-check">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e6" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="queue-has-order" target="queue-pick">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-pick" target="queue-process">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-process" target="queue-success">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e9" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="queue-success" target="queue-done">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e10" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="queue-success" target="queue-retry">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e11" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#ea580c;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#ea580c;" edge="1" parent="1" source="queue-retry" target="queue-back">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="queue-back" target="queue-wait">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="queue-e13" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="queue-retry" target="queue-failed">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="5-puppeteer-automation-flowchart" name="5. Puppeteer Automation Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="950" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="botflow-title" value="&lt;b&gt;Puppeteer Automation Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="botflow-sub" value="Detailed Smile.one browser automation path with session handling and result capture." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="bot-start" value="Worker receives order" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="60" y="220" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="bot-account" value="Select Smile account&lt;br&gt;from pool" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="280" y="210" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-launch" value="Launch browser&lt;br&gt;visible test mode first" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="510" y="210" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-session" value="Load cookies/session&lt;br&gt;userDataDir per account" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="740" y="210" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-open" value="Open Smile.one" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="990" y="210" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-valid" value="Session valid?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1040" y="380" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="bot-login" value="Login manually/automated&lt;br&gt;Save refreshed session" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="800" y="395" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-game" value="Navigate to game page&lt;br&gt;PUBG / MLBB" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="560" y="395" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-fill" value="Fill game ID&lt;br&gt;Validate input" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="320" y="395" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-package" value="Select package&lt;br&gt;UC / Diamonds" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="80" y="395" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-confirm" value="Confirm purchase&lt;br&gt;Final review" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="80" y="590" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="bot-read" value="Read result&lt;br&gt;Success / error text&lt;br&gt;Reference if available" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="320" y="580" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="bot-ok" value="Purchase successful?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="580" y="575" width="160" height="120" as="geometry"/>
</mxCell>
<mxCell id="bot-save-ok" value="Save completed status&lt;br&gt;Update order + queue&lt;br&gt;Update balance" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="810" y="560" width="200" height="120" as="geometry"/>
</mxCell>
<mxCell id="bot-classify" value="Classify failure&lt;br&gt;timeout / CAPTCHA / invalid ID / purchase failed" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="810" y="735" width="220" height="110" as="geometry"/>
</mxCell>
<mxCell id="bot-save-fail" value="Save failed status&lt;br&gt;Retry or manual fallback&lt;br&gt;Alert admin" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1080" y="720" width="200" height="120" as="geometry"/>
</mxCell>
<mxCell id="bot-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-start" target="bot-account">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-account" target="bot-launch">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-launch" target="bot-session">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-session" target="bot-open">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-open" target="bot-valid">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e6" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#ea580c;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#ea580c;" edge="1" parent="1" source="bot-valid" target="bot-login">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-login" target="bot-game">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e8" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="bot-valid" target="bot-game">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-game" target="bot-fill">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-fill" target="bot-package">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-package" target="bot-confirm">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-confirm" target="bot-read">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e13" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-read" target="bot-ok">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e14" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="bot-ok" target="bot-save-ok">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e15" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="bot-ok" target="bot-classify">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="bot-e16" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="bot-classify" target="bot-save-fail">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="6-smile-account-pool-flowchart" name="6. Smile Account Pool Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="pool-title" value="&lt;b&gt;Smile Account Pool Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="pool-sub" value="Multi-account routing using active status, balance, and round robin usage." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="pool-start" value="Worker needs account" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="270" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="pool-table" value="Read smile_accounts table" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="300" y="250" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="pool-active" value="Filter active accounts" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="530" y="250" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="pool-any" value="Any active account?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="240" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="pool-alert" value="No account available&lt;br&gt;Pause job&lt;br&gt;Alert admin" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1000" y="190" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="pool-balance" value="Check balance&lt;br&gt;Enough for package?" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1000" y="380" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="pool-enough" value="Enough balance?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="760" y="390" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="pool-route" value="Apply routing strategy&lt;br&gt;Balance-aware or round robin" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="530" y="420" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="pool-assign" value="Assign account to worker" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="300" y="430" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="pool-update" value="After purchase&lt;br&gt;Update balance&lt;br&gt;Update last_used" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="80" y="420" width="170" height="110" as="geometry"/>
</mxCell>
<mxCell id="pool-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-start" target="pool-table">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-table" target="pool-active">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-active" target="pool-any">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e4" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="pool-any" target="pool-alert">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e5" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="pool-any" target="pool-balance">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-balance" target="pool-enough">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e7" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="pool-enough" target="pool-alert">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e8" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="pool-enough" target="pool-route">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-route" target="pool-assign">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="pool-e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="pool-assign" target="pool-update">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="7-balance-monitoring-flowchart" name="7. Balance Monitoring Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="balance-title" value="&lt;b&gt;Balance Monitoring Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="balance-sub" value="Periodic Smile.one balance checking with warning and critical alerts." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="balance-start" value="Periodic checker&lt;br&gt;every 5 minutes" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="270" width="170" height="80" as="geometry"/>
</mxCell>
<mxCell id="balance-accounts" value="Read active Smile accounts" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="310" y="260" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-read" value="Open/read Smile balance&lt;br&gt;per account" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="550" y="260" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-save" value="Save latest balance" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="800" y="260" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-threshold" value="Below threshold?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1030" y="245" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="balance-ok" value="Balance OK&lt;br&gt;No alert&lt;br&gt;Next interval" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1030" y="450" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-level" value="Select level&lt;br&gt;Warning: 50000 MMK&lt;br&gt;Critical: 10000 MMK" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="780" y="450" width="200" height="110" as="geometry"/>
</mxCell>
<mxCell id="balance-telegram" value="Send Telegram alert" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="530" y="460" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-email" value="Send Email alert" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="310" y="460" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-log" value="Save monitoring log" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="90" y="460" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="balance-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-start" target="balance-accounts">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-accounts" target="balance-read">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-read" target="balance-save">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-save" target="balance-threshold">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e5" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="balance-threshold" target="balance-ok">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e6" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#ea580c;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#ea580c;" edge="1" parent="1" source="balance-threshold" target="balance-level">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-level" target="balance-telegram">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-telegram" target="balance-email">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="balance-e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="balance-email" target="balance-log">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="8-notification-system-flowchart" name="8. Notification System Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="notify-title" value="&lt;b&gt;Notification System Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="notify-sub" value="Telegram and email alerts with typed messages, cooldown, and logging." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="notify-trigger" value="Trigger event&lt;br&gt;low balance / failure / critical / session expired" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="250" width="210" height="100" as="geometry"/>
</mxCell>
<mxCell id="notify-type" value="Select alert type" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="350" y="260" width="170" height="80" as="geometry"/>
</mxCell>
<mxCell id="notify-cooldown" value="Cooldown active?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="580" y="245" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="notify-skip" value="Skip duplicate alert&lt;br&gt;Save skipped log" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="810" y="180" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="notify-compose" value="Compose message&lt;br&gt;Order/account details&lt;br&gt;Error context" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="810" y="360" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="notify-tg" value="Send Telegram" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="570" y="470" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="notify-email" value="Send Email" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="350" y="470" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="notify-result" value="Delivery successful?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="120" y="455" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="notify-log-ok" value="Save sent log" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="120" y="650" width="170" height="80" as="geometry"/>
</mxCell>
<mxCell id="notify-log-fail" value="Save failure log&lt;br&gt;Keep admin-visible" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="350" y="650" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="notify-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-trigger" target="notify-type">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-type" target="notify-cooldown">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e3" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-cooldown" target="notify-skip">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e4" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="notify-cooldown" target="notify-compose">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-compose" target="notify-tg">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-tg" target="notify-email">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="notify-email" target="notify-result">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e8" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="notify-result" target="notify-log-ok">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="notify-e9" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="notify-result" target="notify-log-fail">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="9-error-handling-flowchart" name="9. Error Handling Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="error-title" value="&lt;b&gt;Error Handling Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="error-sub" value="Failure classification, retry, manual fallback, and admin alerting." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="error-start" value="Failure detected" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="250" width="160" height="70" as="geometry"/>
</mxCell>
<mxCell id="error-classify" value="Classify error&lt;br&gt;Browser crash&lt;br&gt;CAPTCHA&lt;br&gt;Timeout&lt;br&gt;Invalid ID&lt;br&gt;Purchase failure" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="300" y="220" width="200" height="130" as="geometry"/>
</mxCell>
<mxCell id="error-browser" value="Browser crash?&lt;br&gt;Restart browser/session" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="560" y="190" width="180" height="100" as="geometry"/>
</mxCell>
<mxCell id="error-captcha" value="CAPTCHA?&lt;br&gt;Pause automation&lt;br&gt;Manual login required" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="560" y="340" width="180" height="100" as="geometry"/>
</mxCell>
<mxCell id="error-retry" value="Retry available?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="820" y="245" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="error-run-retry" value="Retry once&lt;br&gt;Same or new account" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1060" y="190" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="error-success" value="Retry successful?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1060" y="350" width="150" height="120" as="geometry"/>
</mxCell>
<mxCell id="error-complete" value="Mark completed" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="820" y="520" width="170" height="80" as="geometry"/>
</mxCell>
<mxCell id="error-failed" value="Mark failed&lt;br&gt;Manual fallback" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="560" y="520" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="error-alert" value="Alert admin&lt;br&gt;Telegram + Email" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="320" y="520" width="180" height="90" as="geometry"/>
</mxCell>
<mxCell id="error-log" value="Save error log&lt;br&gt;Stack + screenshot&lt;br&gt;Order context" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="80" y="500" width="180" height="120" as="geometry"/>
</mxCell>
<mxCell id="error-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-start" target="error-classify">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-classify" target="error-browser">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-classify" target="error-captcha">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-browser" target="error-retry">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-captcha" target="error-retry">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e6" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#ea580c;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#ea580c;" edge="1" parent="1" source="error-retry" target="error-run-retry">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-run-retry" target="error-success">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e8" value="yes" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#16a34a;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#16a34a;" edge="1" parent="1" source="error-success" target="error-complete">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e9" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="error-success" target="error-failed">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e10" value="no" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#dc2626;" edge="1" parent="1" source="error-retry" target="error-failed">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-failed" target="error-alert">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="error-e12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="error-alert" target="error-log">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="10-admin-dashboard-flowchart" name="10. Admin Dashboard Flowchart">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="admin-title" value="&lt;b&gt;Admin Dashboard Flowchart&lt;/b&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="250" y="30" width="900" height="70" as="geometry"/>
</mxCell>
<mxCell id="admin-sub" value="Admin control panel for payments, orders, balances, retries, and logs." style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="330" y="115" width="740" height="44" as="geometry"/>
</mxCell>
<mxCell id="admin-login" value="Admin login&lt;br&gt;Protected route/token" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="80" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="admin-dashboard" value="Open dashboard" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="310" y="250" width="170" height="90" as="geometry"/>
</mxCell>
<mxCell id="admin-orders" value="View orders&lt;br&gt;pending / paid / completed / failed" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="540" y="170" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="admin-payments" value="Approve payments&lt;br&gt;Confirm / Reject" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="540" y="320" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="admin-balances" value="Monitor balances&lt;br&gt;Warning / critical" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="790" y="170" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="admin-retry" value="Retry failed orders&lt;br&gt;Return to queue" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="790" y="320" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="admin-logs" value="View logs&lt;br&gt;payment / automation / errors" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1040" y="170" width="190" height="100" as="geometry"/>
</mxCell>
<mxCell id="admin-actions" value="Admin action API&lt;br&gt;Update database&lt;br&gt;Trigger notifications" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;" vertex="1" parent="1">
<mxGeometry x="1040" y="320" width="190" height="110" as="geometry"/>
</mxCell>
<mxCell id="admin-done" value="System state updated" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="650" y="560" width="190" height="80" as="geometry"/>
</mxCell>
<mxCell id="admin-e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-login" target="admin-dashboard">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-dashboard" target="admin-orders">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-dashboard" target="admin-payments">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-dashboard" target="admin-balances">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-dashboard" target="admin-retry">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e6" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-dashboard" target="admin-logs">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-orders" target="admin-actions">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-payments" target="admin-actions">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-balances" target="admin-actions">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-retry" target="admin-actions">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e11" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-logs" target="admin-actions">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="admin-e12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=#64748b;" edge="1" parent="1" source="admin-actions" target="admin-done">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,187 @@
<mxfile host="app.diagrams.net" modified="2026-05-10T00:00:00.000Z" agent="Codex" version="24.8.2" type="device">
<diagram id="roadmap" name="Development Roadmap">
<mxGraphModel dx="1600" dy="2600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="2850" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="title" value="&lt;b&gt;Game Top-Up Automation System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 18px;&quot;&gt;Development Roadmap for PUBG + MLBB reseller automation&lt;/font&gt;&lt;br&gt;&lt;font style=&quot;font-size: 13px;&quot;&gt;Smile.one | Puppeteer | Telegram Bot | Email Notifications | KBZPay / AYA Pay Verification&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=26;fontStyle=0;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="1">
<mxGeometry x="140" y="30" width="920" height="100" as="geometry"/>
</mxCell>
<mxCell id="goal" value="&lt;b&gt;Goal:&lt;/b&gt; Build safely | Test stability early | Scale gradually" style="rounded=1;whiteSpace=wrap;html=1;arcSize=10;fillColor=#ecfdf5;strokeColor=#10b981;fontColor=#064e3b;fontSize=16;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="1">
<mxGeometry x="250" y="150" width="700" height="46" as="geometry"/>
</mxCell>
<mxCell id="timeline" value="" style="shape=line;html=1;strokeWidth=4;strokeColor=#94a3b8;" vertex="1" parent="1">
<mxGeometry x="145" y="230" width="0" height="2260" as="geometry"/>
</mxCell>
<mxCell id="n1" value="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dbeafe;strokeColor=#2563eb;fontColor=#1e3a8a;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="230" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p1" value="&lt;b&gt;PHASE 1 - Foundation Setup&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Build the core backend and project structure.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Node.js + Express, server, API routes, env variables, SQLite first, MySQL later, orders/smile_accounts/logs tables, clean folders, GitHub repository.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Backend running, database connected, clean project structure.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="220" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n2" value="2" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fef3c7;strokeColor=#d97706;fontColor=#78350f;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="410" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p2" value="&lt;b&gt;PHASE 2 - Customer Ordering System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Allow customers to place orders.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Homepage, PUBG UC and MLBB Diamonds product pages, game ID input, package selection, payment method selection, order API, order ID generation, unique payment amount logic such as 10000 + 37.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Customers can place orders and receive payment instructions.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fffbeb;strokeColor=#d97706;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="400" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n3" value="3" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fee2e2;strokeColor=#dc2626;fontColor=#7f1d1d;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="590" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p3" value="&lt;b&gt;PHASE 3 - Payment Verification System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Accept KBZPay / AYA Pay screenshot payments.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Upload screenshot and optional transaction ID, create Telegram bot, send screenshot and order details to admin, add Confirm and Reject buttons, manage pending, waiting_verification, paid, completed, failed statuses.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Working payment system with Telegram admin control.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="580" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n4" value="4" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ede9fe;strokeColor=#7c3aed;fontColor=#4c1d95;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="770" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p4" value="&lt;b&gt;PHASE 4 - Browser Automation&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Automate Smile.one purchases. This is the most important stability phase.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Install Puppeteer, open Smile.one, login, navigate pages, enter user ID, select product, confirm purchase, save cookies/session, avoid repeated login, handle timeout, CAPTCHA, invalid ID, and purchase failure.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; System can automatically purchase products.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="760" width="820" height="130" as="geometry"/>
</mxCell>
<mxCell id="n5" value="5" style="ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="960" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p5" value="&lt;b&gt;PHASE 5 - Queue System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Process orders safely and sequentially.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Build order queue, connect New Order -&gt; Queue -&gt; Worker -&gt; Automation, run worker continuously, process one-by-one, retry once after automation failure, fallback to manual processing.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Stable automation flow.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="950" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n6" value="6" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ccfbf1;strokeColor=#0f766e;fontColor=#134e4a;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="1140" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p6" value="&lt;b&gt;PHASE 6 - Smile Account Pool System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Support multiple Smile.one accounts.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Create smile_accounts table, store email, cookies, balance, status, build account manager, select available account, check balance, rotate usage, add round robin logic such as Order 1 -&gt; Account A and Order 2 -&gt; Account B.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Scalable multi-account automation.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdfa;strokeColor=#0f766e;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="1130" width="820" height="125" as="geometry"/>
</mxCell>
<mxCell id="n7" value="7" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="1330" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p7" value="&lt;b&gt;PHASE 7 - Balance Monitoring System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Track Smile balances automatically.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Build balance checker, read Smile account balances, add warning threshold at 50000 MMK, add critical threshold at 10000 MMK, send Telegram alert and email alert.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Real-time balance monitoring.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0f9ff;strokeColor=#0284c7;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="1320" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n8" value="8" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fae8ff;strokeColor=#c026d3;fontColor=#701a75;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="1510" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p8" value="&lt;b&gt;PHASE 8 - Email Notification System&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Create professional system alerts.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Setup Nodemailer with Gmail SMTP, add alert types for low balance, automation failure, and critical errors, add cooldown logic to prevent email spam.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Reliable notification system.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="1500" width="820" height="115" as="geometry"/>
</mxCell>
<mxCell id="n9" value="9" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f1f5f9;strokeColor=#475569;fontColor=#0f172a;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="1685" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p9" value="&lt;b&gt;PHASE 9 - Security and Stability&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Make the system production-ready.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Add HTTPS, protect admin routes, add rate limiting, store automation logs, payment logs, and errors, add backup recovery and manual processing fallback.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Stable and secure system.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="1675" width="820" height="120" as="geometry"/>
</mxCell>
<mxCell id="n10" value="10" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe4e6;strokeColor=#e11d48;fontColor=#881337;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="1870" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p10" value="&lt;b&gt;PHASE 10 - UI and Dashboard&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Improve user experience.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Build admin dashboard with orders, balances, revenue, failed orders; build customer dashboard with order tracking and history; improve mobile responsive design.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; Professional reseller platform.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff1f2;strokeColor=#e11d48;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="1860" width="820" height="115" as="geometry"/>
</mxCell>
<mxCell id="n11" value="11" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fed7aa;strokeColor=#ea580c;fontColor=#7c2d12;fontSize=22;fontStyle=1;" vertex="1" parent="1">
<mxGeometry x="110" y="2045" width="70" height="70" as="geometry"/>
</mxCell>
<mxCell id="p11" value="&lt;b&gt;PHASE 11 - Scaling and Optimization&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 12px;&quot;&gt;&lt;b&gt;Goal:&lt;/b&gt; Prepare for larger traffic.&lt;br&gt;&lt;b&gt;Tasks:&lt;/b&gt; Move to Ubuntu VPS, use Docker, run headless automation, optimize workers, add analytics for sales, profits, and top products.&lt;br&gt;&lt;b&gt;Result:&lt;/b&gt; System is ready for higher traffic and operational scaling.&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=15;align=left;verticalAlign=top;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="2035" width="820" height="115" as="geometry"/>
</mxCell>
<mxCell id="final" value="&lt;b&gt;FINAL SYSTEM&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 13px;&quot;&gt;PUBG + MLBB top-ups | KBZPay / AYA Pay payments | Telegram verification | Smile.one automation | Multi-account support | Balance monitoring | Email notifications | Queue processing | Admin dashboard | Automated purchasing&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#111827;strokeColor=#111827;fontColor=#ffffff;fontSize=20;align=center;verticalAlign=middle;spacing=14;" vertex="1" parent="1">
<mxGeometry x="220" y="2250" width="820" height="110" as="geometry"/>
</mxCell>
<mxCell id="e1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p1" target="p2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p2" target="p3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p3" target="p4">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p4" target="p5">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p5" target="p6">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p6" target="p7">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p7" target="p8">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p8" target="p9">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p9" target="p10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p10" target="p11">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#94a3b8;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="p11" target="final">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="architecture" name="Final System Flow">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1400" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="a0"/>
<mxCell id="a1" parent="a0"/>
<mxCell id="a-title" value="&lt;b&gt;Final System Flow&lt;/b&gt;&lt;br&gt;&lt;font style=&quot;font-size: 14px;&quot;&gt;Customer order to payment verification to queue-driven Smile.one automation&lt;/font&gt;" style="rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;align=center;verticalAlign=middle;spacing=12;" vertex="1" parent="a1">
<mxGeometry x="250" y="30" width="900" height="80" as="geometry"/>
</mxCell>
<mxCell id="cust" value="&lt;b&gt;Customer&lt;/b&gt;&lt;br&gt;Selects PUBG / MLBB&lt;br&gt;Enters game ID&lt;br&gt;Uploads payment proof" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#eff6ff;strokeColor=#2563eb;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="60" y="190" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="front" value="&lt;b&gt;Frontend&lt;/b&gt;&lt;br&gt;Homepage&lt;br&gt;Product pages&lt;br&gt;Order tracking" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fffbeb;strokeColor=#d97706;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="290" y="190" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="api" value="&lt;b&gt;Backend API&lt;/b&gt;&lt;br&gt;Orders&lt;br&gt;Payments&lt;br&gt;Admin actions" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0fdf4;strokeColor=#16a34a;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="520" y="190" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="db" value="&lt;b&gt;Database&lt;/b&gt;&lt;br&gt;orders&lt;br&gt;smile_accounts&lt;br&gt;logs&lt;br&gt;order_queue" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="750" y="175" width="170" height="130" as="geometry"/>
</mxCell>
<mxCell id="queue" value="&lt;b&gt;Order Queue&lt;/b&gt;&lt;br&gt;waiting&lt;br&gt;processing&lt;br&gt;completed&lt;br&gt;failed" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="980" y="190" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="worker" value="&lt;b&gt;Worker&lt;/b&gt;&lt;br&gt;Processes one-by-one&lt;br&gt;Retries once&lt;br&gt;Manual fallback" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ede9fe;strokeColor=#7c3aed;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="980" y="380" width="170" height="110" as="geometry"/>
</mxCell>
<mxCell id="puppeteer" value="&lt;b&gt;Puppeteer&lt;/b&gt;&lt;br&gt;Saved sessions&lt;br&gt;Visible test mode&lt;br&gt;Headless later" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f3ff;strokeColor=#7c3aed;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="750" y="390" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="smile" value="&lt;b&gt;Smile.one&lt;/b&gt;&lt;br&gt;Login session&lt;br&gt;Game page&lt;br&gt;Purchase confirmation" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fae8ff;strokeColor=#c026d3;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="520" y="390" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="accounts" value="&lt;b&gt;Smile Account Pool&lt;/b&gt;&lt;br&gt;email&lt;br&gt;cookies/session&lt;br&gt;balance&lt;br&gt;status&lt;br&gt;round robin" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f0fdfa;strokeColor=#0f766e;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="1190" y="360" width="170" height="145" as="geometry"/>
</mxCell>
<mxCell id="wallets" value="&lt;b&gt;Payments&lt;/b&gt;&lt;br&gt;KBZPay&lt;br&gt;AYA Pay&lt;br&gt;Manual verification" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fef2f2;strokeColor=#dc2626;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="290" y="390" width="170" height="100" as="geometry"/>
</mxCell>
<mxCell id="telegram" value="&lt;b&gt;Telegram Bot&lt;/b&gt;&lt;br&gt;Order details&lt;br&gt;Screenshot&lt;br&gt;Confirm / Reject&lt;br&gt;Admin controls" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="60" y="390" width="170" height="120" as="geometry"/>
</mxCell>
<mxCell id="monitor" value="&lt;b&gt;Live Checking&lt;/b&gt;&lt;br&gt;Health checks&lt;br&gt;Balance checks&lt;br&gt;Queue errors&lt;br&gt;Session expiry" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f1f5f9;strokeColor=#475569;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="520" y="620" width="170" height="120" as="geometry"/>
</mxCell>
<mxCell id="email" value="&lt;b&gt;Email Notifications&lt;/b&gt;&lt;br&gt;Low balance&lt;br&gt;Automation failure&lt;br&gt;Critical errors&lt;br&gt;Cooldown" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fdf4ff;strokeColor=#c026d3;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="750" y="620" width="170" height="120" as="geometry"/>
</mxCell>
<mxCell id="dashboard" value="&lt;b&gt;Admin Dashboard&lt;/b&gt;&lt;br&gt;Orders&lt;br&gt;Balances&lt;br&gt;Revenue&lt;br&gt;Failed jobs" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff1f2;strokeColor=#e11d48;fontSize=14;align=center;verticalAlign=middle;spacing=10;" vertex="1" parent="a1">
<mxGeometry x="980" y="620" width="170" height="120" as="geometry"/>
</mxCell>
<mxCell id="ae1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="cust" target="front"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="front" target="api"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="api" target="db"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="db" target="queue"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="queue" target="worker"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="worker" target="puppeteer"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#64748b;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="puppeteer" target="smile"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="wallets" target="telegram"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae9" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#dc2626;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="telegram" target="api"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae10" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#0f766e;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="accounts" target="worker"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#475569;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="monitor" target="email"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#475569;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="monitor" target="telegram"><mxGeometry relative="1" as="geometry"/></mxCell>
<mxCell id="ae13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#e11d48;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="a1" source="dashboard" target="api"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,361 @@
const fs = require("fs");
const path = require("path");
const outFile = path.join(__dirname, "game-topup-automation-flowcharts.drawio");
function esc(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function node(id, value, x, y, w, h, style) {
return ` <mxCell id="${id}" value="${esc(value)}" style="${style}" vertex="1" parent="1">
<mxGeometry x="${x}" y="${y}" width="${w}" height="${h}" as="geometry"/>
</mxCell>`;
}
function edge(id, from, to, label = "", color = "#64748b") {
return ` <mxCell id="${id}" value="${esc(label)}" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=${color};strokeWidth=2;endArrow=block;endFill=1;fontSize=11;fontColor=${color};" edge="1" parent="1" source="${from}" target="${to}">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`;
}
const styles = {
title:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#0f172a;strokeColor=#0f172a;fontColor=#ffffff;fontSize=24;fontStyle=1;align=center;verticalAlign=middle;spacing=12;",
subtitle:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f8fafc;strokeColor=#cbd5e1;fontColor=#334155;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
start:
"ellipse;whiteSpace=wrap;html=1;fillColor=#dcfce7;strokeColor=#16a34a;fontColor=#14532d;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;",
end:
"ellipse;whiteSpace=wrap;html=1;fillColor=#e0f2fe;strokeColor=#0284c7;fontColor=#0c4a6e;fontSize=14;fontStyle=1;align=center;verticalAlign=middle;",
process:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#eff6ff;strokeColor=#2563eb;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
backend:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f0fdf4;strokeColor=#16a34a;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
automation:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f5f3ff;strokeColor=#7c3aed;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
payment:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fef2f2;strokeColor=#dc2626;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
notify:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fdf4ff;strokeColor=#c026d3;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
warning:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#fff7ed;strokeColor=#ea580c;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
decision:
"rhombus;whiteSpace=wrap;html=1;fillColor=#fef9c3;strokeColor=#ca8a04;fontColor=#713f12;fontSize=12;align=center;verticalAlign=middle;spacing=8;",
data:
"shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#f8fafc;strokeColor=#475569;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
external:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#ecfeff;strokeColor=#0891b2;fontColor=#164e63;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
manual:
"rounded=1;whiteSpace=wrap;html=1;arcSize=8;fillColor=#f1f5f9;strokeColor=#64748b;fontColor=#111827;fontSize=13;align=center;verticalAlign=middle;spacing=8;",
};
function diagram(name, cells, width = 1400, height = 900) {
return ` <diagram id="${esc(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"))}" name="${esc(name)}">
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="${width}" pageHeight="${height}" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
${cells.join("\n")}
</root>
</mxGraphModel>
</diagram>`;
}
function titleCells(prefix, title, subtitle) {
return [
node(`${prefix}-title`, `<b>${title}</b>`, 250, 30, 900, 70, styles.title),
node(`${prefix}-sub`, subtitle, 330, 115, 740, 44, styles.subtitle),
];
}
const diagrams = [];
diagrams.push(
diagram("1. Overall System Architecture", [
...titleCells("arch", "Overall System Architecture", "Master diagram: customer order, payment verification, queue automation, monitoring, and notifications."),
node("arch-customer", "<b>Customer</b><br>Places order<br>Uploads payment proof<br>Tracks status", 50, 230, 170, 105, styles.start),
node("arch-website", "<b>Website</b><br>Frontend pages<br>Order form<br>Tracking UI", 280, 230, 170, 105, styles.process),
node("arch-api", "<b>Backend API</b><br>Orders<br>Payments<br>Admin routes", 510, 230, 170, 105, styles.backend),
node("arch-db", "<b>Database</b><br>orders<br>smile_accounts<br>logs<br>order_queue", 740, 215, 170, 130, styles.data),
node("arch-queue", "<b>Queue</b><br>waiting<br>processing<br>completed<br>failed", 970, 230, 170, 105, styles.backend),
node("arch-worker", "<b>Puppeteer Worker</b><br>Reads queue<br>Runs automation<br>Updates status", 970, 430, 170, 115, styles.automation),
node("arch-smile", "<b>Smile.one</b><br>Login/session<br>Game top-up<br>Purchase result", 740, 430, 170, 115, styles.external),
node("arch-telegram", "<b>Telegram Bot</b><br>Payment proof<br>Confirm / Reject<br>Admin alerts", 280, 430, 170, 115, styles.notify),
node("arch-email", "<b>Email System</b><br>Low balance<br>Failures<br>Critical errors", 510, 620, 170, 115, styles.notify),
node("arch-admin", "<b>Admin</b><br>Checks wallet<br>Controls orders<br>Retries failures", 50, 430, 170, 115, styles.manual),
edge("arch-e1", "arch-customer", "arch-website"),
edge("arch-e2", "arch-website", "arch-api"),
edge("arch-e3", "arch-api", "arch-db"),
edge("arch-e4", "arch-db", "arch-queue"),
edge("arch-e5", "arch-queue", "arch-worker"),
edge("arch-e6", "arch-worker", "arch-smile"),
edge("arch-e7", "arch-api", "arch-telegram", "payment notification", "#c026d3"),
edge("arch-e8", "arch-telegram", "arch-admin", "admin action", "#c026d3"),
edge("arch-e9", "arch-admin", "arch-api", "confirm / reject", "#64748b"),
edge("arch-e10", "arch-worker", "arch-email", "failure alerts", "#c026d3"),
edge("arch-e11", "arch-smile", "arch-db", "result + balance", "#0891b2"),
])
);
diagrams.push(
diagram("2. Customer Order Flowchart", [
...titleCells("order", "Customer Order Flowchart", "How a customer creates an order and receives payment instructions."),
node("order-start", "Start", 90, 260, 120, 70, styles.start),
node("order-game", "Select game<br><b>PUBG UC</b> or <b>MLBB Diamonds</b>", 270, 250, 170, 90, styles.process),
node("order-id", "Enter game ID<br>and server info if needed", 500, 250, 170, 90, styles.process),
node("order-package", "Select package<br>UC / Diamonds amount", 730, 250, 170, 90, styles.process),
node("order-payment", "Select payment method<br>KBZPay / AYA Pay", 960, 250, 170, 90, styles.payment),
node("order-api", "Create order API<br>Validate fields", 270, 450, 170, 90, styles.backend),
node("order-amount", "Generate amount<br>Base price + random digits<br>Example: 10000 + 37", 500, 440, 190, 110, styles.backend),
node("order-store", "Store order<br>Status: pending", 760, 450, 170, 90, styles.data),
node("order-show", "Show payment instructions<br>Order ID<br>Exact amount<br>Wallet number", 990, 440, 190, 110, styles.end),
edge("order-e1", "order-start", "order-game"),
edge("order-e2", "order-game", "order-id"),
edge("order-e3", "order-id", "order-package"),
edge("order-e4", "order-package", "order-payment"),
edge("order-e5", "order-payment", "order-api"),
edge("order-e6", "order-api", "order-amount"),
edge("order-e7", "order-amount", "order-store"),
edge("order-e8", "order-store", "order-show"),
])
);
diagrams.push(
diagram("3. Payment Verification Flowchart", [
...titleCells("pay", "Payment Verification Flowchart", "KBZPay / AYA Pay screenshot verification with Telegram admin control."),
node("pay-start", "Customer pays exact amount", 80, 250, 160, 80, styles.start),
node("pay-upload", "Upload screenshot<br>Transaction ID optional", 300, 250, 170, 90, styles.payment),
node("pay-store-img", "Store image<br>backend/uploads", 530, 250, 170, 90, styles.data),
node("pay-status", "Update order<br>Status: waiting_verification", 760, 250, 190, 90, styles.backend),
node("pay-telegram", "Send Telegram notification<br>Order + screenshot + buttons", 1010, 240, 200, 110, styles.notify),
node("pay-wallet", "Admin checks wallet app<br>Amount / method / time", 300, 470, 190, 100, styles.manual),
node("pay-valid", "Payment valid?", 560, 460, 150, 120, styles.decision),
node("pay-confirm", "Confirm payment<br>Status: paid<br>Add to queue", 800, 450, 190, 110, styles.backend),
node("pay-reject", "Reject payment<br>Status: rejected<br>Customer can contact support", 800, 610, 190, 110, styles.warning),
node("pay-log", "Save verification log", 1050, 520, 170, 90, styles.data),
edge("pay-e1", "pay-start", "pay-upload"),
edge("pay-e2", "pay-upload", "pay-store-img"),
edge("pay-e3", "pay-store-img", "pay-status"),
edge("pay-e4", "pay-status", "pay-telegram"),
edge("pay-e5", "pay-telegram", "pay-wallet"),
edge("pay-e6", "pay-wallet", "pay-valid"),
edge("pay-e7", "pay-valid", "pay-confirm", "yes", "#16a34a"),
edge("pay-e8", "pay-valid", "pay-reject", "no", "#dc2626"),
edge("pay-e9", "pay-confirm", "pay-log"),
edge("pay-e10", "pay-reject", "pay-log"),
])
);
diagrams.push(
diagram("4. Queue Processing Flowchart", [
...titleCells("queue", "Queue Processing Flowchart", "Safe sequential processing with retry and manual fallback."),
node("queue-new", "Paid order", 80, 260, 140, 70, styles.start),
node("queue-wait", "Waiting queue<br>status: waiting", 280, 250, 170, 90, styles.backend),
node("queue-worker-check", "Worker checks queue<br>interval loop", 520, 250, 170, 90, styles.automation),
node("queue-has-order", "Waiting order found?", 760, 235, 150, 120, styles.decision),
node("queue-idle", "No order<br>sleep and check again", 1010, 170, 170, 90, styles.manual),
node("queue-pick", "Pick one order<br>lock / mark processing", 1010, 360, 190, 90, styles.automation),
node("queue-process", "Run Puppeteer automation", 760, 520, 190, 90, styles.automation),
node("queue-success", "Success?", 520, 505, 150, 120, styles.decision),
node("queue-done", "Mark completed<br>Save result", 280, 440, 170, 90, styles.end),
node("queue-retry", "Attempts left?", 280, 610, 150, 120, styles.decision),
node("queue-back", "Retry once<br>Return to waiting", 520, 660, 170, 90, styles.warning),
node("queue-failed", "Mark failed<br>Manual fallback<br>Alert admin", 760, 660, 190, 100, styles.payment),
edge("queue-e1", "queue-new", "queue-wait"),
edge("queue-e2", "queue-wait", "queue-worker-check"),
edge("queue-e3", "queue-worker-check", "queue-has-order"),
edge("queue-e4", "queue-has-order", "queue-idle", "no", "#64748b"),
edge("queue-e5", "queue-idle", "queue-worker-check", "loop", "#64748b"),
edge("queue-e6", "queue-has-order", "queue-pick", "yes", "#16a34a"),
edge("queue-e7", "queue-pick", "queue-process"),
edge("queue-e8", "queue-process", "queue-success"),
edge("queue-e9", "queue-success", "queue-done", "yes", "#16a34a"),
edge("queue-e10", "queue-success", "queue-retry", "no", "#dc2626"),
edge("queue-e11", "queue-retry", "queue-back", "yes", "#ea580c"),
edge("queue-e12", "queue-back", "queue-wait"),
edge("queue-e13", "queue-retry", "queue-failed", "no", "#dc2626"),
])
);
diagrams.push(
diagram("5. Puppeteer Automation Flowchart", [
...titleCells("botflow", "Puppeteer Automation Flowchart", "Detailed Smile.one browser automation path with session handling and result capture."),
node("bot-start", "Worker receives order", 60, 220, 160, 70, styles.start),
node("bot-account", "Select Smile account<br>from pool", 280, 210, 170, 90, styles.automation),
node("bot-launch", "Launch browser<br>visible test mode first", 510, 210, 170, 90, styles.automation),
node("bot-session", "Load cookies/session<br>userDataDir per account", 740, 210, 190, 90, styles.automation),
node("bot-open", "Open Smile.one", 990, 210, 170, 90, styles.external),
node("bot-valid", "Session valid?", 1040, 380, 150, 120, styles.decision),
node("bot-login", "Login manually/automated<br>Save refreshed session", 800, 395, 190, 90, styles.warning),
node("bot-game", "Navigate to game page<br>PUBG / MLBB", 560, 395, 190, 90, styles.external),
node("bot-fill", "Fill game ID<br>Validate input", 320, 395, 170, 90, styles.automation),
node("bot-package", "Select package<br>UC / Diamonds", 80, 395, 170, 90, styles.automation),
node("bot-confirm", "Confirm purchase<br>Final review", 80, 590, 170, 90, styles.payment),
node("bot-read", "Read result<br>Success / error text<br>Reference if available", 320, 580, 190, 110, styles.automation),
node("bot-ok", "Purchase successful?", 580, 575, 160, 120, styles.decision),
node("bot-save-ok", "Save completed status<br>Update order + queue<br>Update balance", 810, 560, 200, 120, styles.end),
node("bot-classify", "Classify failure<br>timeout / CAPTCHA / invalid ID / purchase failed", 810, 735, 220, 110, styles.payment),
node("bot-save-fail", "Save failed status<br>Retry or manual fallback<br>Alert admin", 1080, 720, 200, 120, styles.warning),
edge("bot-e1", "bot-start", "bot-account"),
edge("bot-e2", "bot-account", "bot-launch"),
edge("bot-e3", "bot-launch", "bot-session"),
edge("bot-e4", "bot-session", "bot-open"),
edge("bot-e5", "bot-open", "bot-valid"),
edge("bot-e6", "bot-valid", "bot-login", "no", "#ea580c"),
edge("bot-e7", "bot-login", "bot-game"),
edge("bot-e8", "bot-valid", "bot-game", "yes", "#16a34a"),
edge("bot-e9", "bot-game", "bot-fill"),
edge("bot-e10", "bot-fill", "bot-package"),
edge("bot-e11", "bot-package", "bot-confirm"),
edge("bot-e12", "bot-confirm", "bot-read"),
edge("bot-e13", "bot-read", "bot-ok"),
edge("bot-e14", "bot-ok", "bot-save-ok", "yes", "#16a34a"),
edge("bot-e15", "bot-ok", "bot-classify", "no", "#dc2626"),
edge("bot-e16", "bot-classify", "bot-save-fail"),
], 1400, 950)
);
diagrams.push(
diagram("6. Smile Account Pool Flowchart", [
...titleCells("pool", "Smile Account Pool Flowchart", "Multi-account routing using active status, balance, and round robin usage."),
node("pool-start", "Worker needs account", 80, 270, 160, 70, styles.start),
node("pool-table", "Read smile_accounts table", 300, 250, 170, 100, styles.data),
node("pool-active", "Filter active accounts", 530, 250, 170, 100, styles.automation),
node("pool-any", "Any active account?", 760, 240, 150, 120, styles.decision),
node("pool-alert", "No account available<br>Pause job<br>Alert admin", 1000, 190, 190, 100, styles.payment),
node("pool-balance", "Check balance<br>Enough for package?", 1000, 380, 190, 100, styles.automation),
node("pool-enough", "Enough balance?", 760, 390, 150, 120, styles.decision),
node("pool-route", "Apply routing strategy<br>Balance-aware or round robin", 530, 420, 190, 100, styles.backend),
node("pool-assign", "Assign account to worker", 300, 430, 170, 90, styles.end),
node("pool-update", "After purchase<br>Update balance<br>Update last_used", 80, 420, 170, 110, styles.data),
edge("pool-e1", "pool-start", "pool-table"),
edge("pool-e2", "pool-table", "pool-active"),
edge("pool-e3", "pool-active", "pool-any"),
edge("pool-e4", "pool-any", "pool-alert", "no", "#dc2626"),
edge("pool-e5", "pool-any", "pool-balance", "yes", "#16a34a"),
edge("pool-e6", "pool-balance", "pool-enough"),
edge("pool-e7", "pool-enough", "pool-alert", "no", "#dc2626"),
edge("pool-e8", "pool-enough", "pool-route", "yes", "#16a34a"),
edge("pool-e9", "pool-route", "pool-assign"),
edge("pool-e10", "pool-assign", "pool-update"),
])
);
diagrams.push(
diagram("7. Balance Monitoring Flowchart", [
...titleCells("balance", "Balance Monitoring Flowchart", "Periodic Smile.one balance checking with warning and critical alerts."),
node("balance-start", "Periodic checker<br>every 5 minutes", 80, 270, 170, 80, styles.start),
node("balance-accounts", "Read active Smile accounts", 310, 260, 180, 90, styles.data),
node("balance-read", "Open/read Smile balance<br>per account", 550, 260, 190, 90, styles.automation),
node("balance-save", "Save latest balance", 800, 260, 170, 90, styles.data),
node("balance-threshold", "Below threshold?", 1030, 245, 150, 120, styles.decision),
node("balance-ok", "Balance OK<br>No alert<br>Next interval", 1030, 450, 170, 90, styles.end),
node("balance-level", "Select level<br>Warning: 50000 MMK<br>Critical: 10000 MMK", 780, 450, 200, 110, styles.warning),
node("balance-telegram", "Send Telegram alert", 530, 460, 170, 90, styles.notify),
node("balance-email", "Send Email alert", 310, 460, 170, 90, styles.notify),
node("balance-log", "Save monitoring log", 90, 460, 170, 90, styles.data),
edge("balance-e1", "balance-start", "balance-accounts"),
edge("balance-e2", "balance-accounts", "balance-read"),
edge("balance-e3", "balance-read", "balance-save"),
edge("balance-e4", "balance-save", "balance-threshold"),
edge("balance-e5", "balance-threshold", "balance-ok", "no", "#16a34a"),
edge("balance-e6", "balance-threshold", "balance-level", "yes", "#ea580c"),
edge("balance-e7", "balance-level", "balance-telegram"),
edge("balance-e8", "balance-telegram", "balance-email"),
edge("balance-e9", "balance-email", "balance-log"),
])
);
diagrams.push(
diagram("8. Notification System Flowchart", [
...titleCells("notify", "Notification System Flowchart", "Telegram and email alerts with typed messages, cooldown, and logging."),
node("notify-trigger", "Trigger event<br>low balance / failure / critical / session expired", 80, 250, 210, 100, styles.start),
node("notify-type", "Select alert type", 350, 260, 170, 80, styles.notify),
node("notify-cooldown", "Cooldown active?", 580, 245, 150, 120, styles.decision),
node("notify-skip", "Skip duplicate alert<br>Save skipped log", 810, 180, 190, 90, styles.manual),
node("notify-compose", "Compose message<br>Order/account details<br>Error context", 810, 360, 190, 110, styles.notify),
node("notify-tg", "Send Telegram", 570, 470, 170, 90, styles.notify),
node("notify-email", "Send Email", 350, 470, 170, 90, styles.notify),
node("notify-result", "Delivery successful?", 120, 455, 150, 120, styles.decision),
node("notify-log-ok", "Save sent log", 120, 650, 170, 80, styles.data),
node("notify-log-fail", "Save failure log<br>Keep admin-visible", 350, 650, 190, 90, styles.payment),
edge("notify-e1", "notify-trigger", "notify-type"),
edge("notify-e2", "notify-type", "notify-cooldown"),
edge("notify-e3", "notify-cooldown", "notify-skip", "yes", "#64748b"),
edge("notify-e4", "notify-cooldown", "notify-compose", "no", "#16a34a"),
edge("notify-e5", "notify-compose", "notify-tg"),
edge("notify-e6", "notify-tg", "notify-email"),
edge("notify-e7", "notify-email", "notify-result"),
edge("notify-e8", "notify-result", "notify-log-ok", "yes", "#16a34a"),
edge("notify-e9", "notify-result", "notify-log-fail", "no", "#dc2626"),
])
);
diagrams.push(
diagram("9. Error Handling Flowchart", [
...titleCells("error", "Error Handling Flowchart", "Failure classification, retry, manual fallback, and admin alerting."),
node("error-start", "Failure detected", 80, 250, 160, 70, styles.start),
node("error-classify", "Classify error<br>Browser crash<br>CAPTCHA<br>Timeout<br>Invalid ID<br>Purchase failure", 300, 220, 200, 130, styles.payment),
node("error-browser", "Browser crash?<br>Restart browser/session", 560, 190, 180, 100, styles.warning),
node("error-captcha", "CAPTCHA?<br>Pause automation<br>Manual login required", 560, 340, 180, 100, styles.warning),
node("error-retry", "Retry available?", 820, 245, 150, 120, styles.decision),
node("error-run-retry", "Retry once<br>Same or new account", 1060, 190, 180, 90, styles.automation),
node("error-success", "Retry successful?", 1060, 350, 150, 120, styles.decision),
node("error-complete", "Mark completed", 820, 520, 170, 80, styles.end),
node("error-failed", "Mark failed<br>Manual fallback", 560, 520, 180, 90, styles.payment),
node("error-alert", "Alert admin<br>Telegram + Email", 320, 520, 180, 90, styles.notify),
node("error-log", "Save error log<br>Stack + screenshot<br>Order context", 80, 500, 180, 120, styles.data),
edge("error-e1", "error-start", "error-classify"),
edge("error-e2", "error-classify", "error-browser"),
edge("error-e3", "error-classify", "error-captcha"),
edge("error-e4", "error-browser", "error-retry"),
edge("error-e5", "error-captcha", "error-retry"),
edge("error-e6", "error-retry", "error-run-retry", "yes", "#ea580c"),
edge("error-e7", "error-run-retry", "error-success"),
edge("error-e8", "error-success", "error-complete", "yes", "#16a34a"),
edge("error-e9", "error-success", "error-failed", "no", "#dc2626"),
edge("error-e10", "error-retry", "error-failed", "no", "#dc2626"),
edge("error-e11", "error-failed", "error-alert"),
edge("error-e12", "error-alert", "error-log"),
])
);
diagrams.push(
diagram("10. Admin Dashboard Flowchart", [
...titleCells("admin", "Admin Dashboard Flowchart", "Admin control panel for payments, orders, balances, retries, and logs."),
node("admin-login", "Admin login<br>Protected route/token", 80, 250, 170, 90, styles.start),
node("admin-dashboard", "Open dashboard", 310, 250, 170, 90, styles.process),
node("admin-orders", "View orders<br>pending / paid / completed / failed", 540, 170, 190, 100, styles.backend),
node("admin-payments", "Approve payments<br>Confirm / Reject", 540, 320, 190, 90, styles.payment),
node("admin-balances", "Monitor balances<br>Warning / critical", 790, 170, 190, 100, styles.external),
node("admin-retry", "Retry failed orders<br>Return to queue", 790, 320, 190, 90, styles.warning),
node("admin-logs", "View logs<br>payment / automation / errors", 1040, 170, 190, 100, styles.data),
node("admin-actions", "Admin action API<br>Update database<br>Trigger notifications", 1040, 320, 190, 110, styles.backend),
node("admin-done", "System state updated", 650, 560, 190, 80, styles.end),
edge("admin-e1", "admin-login", "admin-dashboard"),
edge("admin-e2", "admin-dashboard", "admin-orders"),
edge("admin-e3", "admin-dashboard", "admin-payments"),
edge("admin-e4", "admin-dashboard", "admin-balances"),
edge("admin-e5", "admin-dashboard", "admin-retry"),
edge("admin-e6", "admin-dashboard", "admin-logs"),
edge("admin-e7", "admin-orders", "admin-actions"),
edge("admin-e8", "admin-payments", "admin-actions"),
edge("admin-e9", "admin-balances", "admin-actions"),
edge("admin-e10", "admin-retry", "admin-actions"),
edge("admin-e11", "admin-logs", "admin-actions"),
edge("admin-e12", "admin-actions", "admin-done"),
])
);
const xml = `<mxfile host="app.diagrams.net" modified="2026-05-11T00:00:00.000Z" agent="Codex" version="24.8.2" type="device">
${diagrams.join("\n")}
</mxfile>
`;
fs.writeFileSync(outFile, xml);
console.log(outFile);
+49
View File
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin - Game Topup</title>
<meta http-equiv="Cache-Control" content="no-store">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="/styles.css?v=20260428-10">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home">
<span class="logo-mark">🎮</span>
<span><strong>GAME TOPUP</strong><small>ADMIN ONLY</small></span>
</a>
<nav class="main-nav" aria-label="Admin navigation">
<a href="/">Public Site</a>
<a class="active" href="/admin.html">Admin</a>
</nav>
</header>
<main class="page-main">
<section class="page-title">
<h1>ADMIN <span>DASHBOARD</span></h1>
<p>Private order verification and fulfillment controls.</p>
</section>
<section class="panel admin-panel single-panel">
<h2>Orders</h2>
<form id="adminForm" class="inline-form">
<input name="token" type="password" placeholder="Admin token" autocomplete="current-password">
<button type="submit">Load</button>
</form>
<div id="adminOrders" class="admin-orders"></div>
</section>
</main>
<div class="modal-backdrop" id="modalBackdrop" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button>
<h2 id="modalTitle">Notice</h2>
<div id="modalBody"></div>
</section>
</div>
<script src="/app.js?v=20260428-10"></script>
</body>
</html>
+341
View File
@@ -0,0 +1,341 @@
let config = null;
let activeOrderId = null;
let timerHandle = null;
let selectedCurrency = "MMK";
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
async function api(path, options = {}) {
const response = await fetch(path, options);
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "Request failed");
return payload;
}
function money(amount) {
if (selectedCurrency === "USD") return `$${(Number(amount) / 3500).toFixed(2)}`;
return `${Number(amount).toLocaleString()} MMK`;
}
function showModal(title, body) {
if (!$("#modalBackdrop")) return alert(title);
$("#modalTitle").textContent = title;
$("#modalBody").innerHTML = body;
$("#modalBackdrop").hidden = false;
}
function closeModal() {
if ($("#modalBackdrop")) $("#modalBackdrop").hidden = true;
}
function setStatus(status) {
if (!$("#statusBadge")) return;
$("#statusBadge").textContent = status;
$("#statusBadge").className = `badge ${status}`;
}
function renderProducts() {
if (!config || !$("#gameSelect") || !$("#productSelect")) return;
const game = $("#gameSelect").value;
const products = config.menu[game] || {};
$("#productSelect").innerHTML = Object.entries(products)
.map(([name, price]) => `<option value="${name}">${name} - ${money(price)}</option>`)
.join("");
renderProductPreview();
if ($("#selectedGameTitle")) $("#selectedGameTitle").textContent = game || "Choose Product";
}
function renderProductPreview() {
if (!config || !$("#productPreview") || !$("#gameSelect") || !$("#productSelect")) return;
const game = $("#gameSelect").value;
const product = $("#productSelect").value;
const price = config.menu[game]?.[product];
$("#productPreview").textContent = price
? `${product}: ${money(price)} before unique payment digits`
: "Select a product to see price.";
}
function renderTrack(order, target) {
if (!target) return;
const completeText =
order.status === "completed"
? `<div class="notice">Your order has been completed successfully.</div>`
: "";
target.innerHTML = `
<div class="order-card">
<header>
<strong>${order.order_id}</strong>
<span class="badge ${order.status}">${order.status}</span>
</header>
<div class="order-meta">
<span>Game: ${order.game}</span>
<span>User ID: ${order.user_game_id}</span>
<span>Product: ${order.product}</span>
<span>Amount: ${money(order.amount)}</span>
<span>Payment: ${order.payment_method || "-"}</span>
<span>Created: ${order.created_at}</span>
</div>
${completeText}
</div>
`;
}
async function refreshOrder(orderId) {
if (!orderId) return;
const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`);
setStatus(order.status);
renderTrack(order, $("#trackResult"));
}
function startTimer(expiresAt) {
if (!$("#timer")) return;
clearInterval(timerHandle);
const tick = () => {
const remaining = Math.max(0, expiresAt - Math.floor(Date.now() / 1000));
const mins = String(Math.floor(remaining / 60)).padStart(2, "0");
const secs = String(remaining % 60).padStart(2, "0");
$("#timer").textContent = `${mins}:${secs}`;
if (remaining === 0) {
clearInterval(timerHandle);
refreshOrder(activeOrderId);
}
};
tick();
timerHandle = setInterval(tick, 1000);
}
function renderAdminOrder(order, token) {
const screenshot = order.screenshot
? `<a href="/${order.screenshot}" target="_blank" rel="noreferrer">View screenshot</a>`
: `<span>No screenshot</span>`;
return `
<div class="order-card">
<header>
<strong>${order.order_id}</strong>
<span class="badge ${order.status}">${order.status}</span>
</header>
<div class="order-meta">
<span>${order.game}</span>
<span>${order.user_game_id}</span>
<span>${order.product}</span>
<span>${money(order.amount)}</span>
<span>${order.payment_method || "-"}</span>
${screenshot}
</div>
<div class="actions">
<button data-admin-action="confirm" data-order-id="${order.order_id}" data-token="${token}">Confirm</button>
<button class="danger" data-admin-action="reject" data-order-id="${order.order_id}" data-token="${token}">Reject</button>
<button class="secondary" data-admin-action="complete" data-order-id="${order.order_id}" data-token="${token}">Completed</button>
</div>
</div>
`;
}
function renderPricingPage() {
if (!config || !$("#pricingGrid")) return;
$("#pricingGrid").innerHTML = Object.entries(config.menu)
.map(([game, products]) => {
const rows = Object.entries(products)
.map(([product, price]) => `
<div class="price-row">
<span>${product}</span>
<strong>${money(price)}</strong>
</div>
`)
.join("");
return `
<section class="panel price-panel">
<h2>${game}</h2>
<div class="price-list">${rows}</div>
<a class="price-action" href="/orders.html?game=${encodeURIComponent(game)}">Top Up ${game}</a>
</section>
`;
})
.join("");
}
function setupSharedControls() {
$("#currencyButton")?.addEventListener("click", () => {
const menu = $("#currencyMenu");
if (!menu) return;
menu.hidden = !menu.hidden;
$("#currencyButton").setAttribute("aria-expanded", String(!menu.hidden));
});
$$("[data-currency]").forEach((button) => {
button.addEventListener("click", () => {
selectedCurrency = button.dataset.currency;
if ($("#currencyButton")) $("#currencyButton").children[1].textContent = selectedCurrency;
if ($("#currencyMenu")) $("#currencyMenu").hidden = true;
renderProductPreview();
renderPricingPage();
if (activeOrderId) refreshOrder(activeOrderId);
});
});
$("#loginButton")?.addEventListener("click", () => {
showModal("Login / Register", "<p>Account login is not required for checkout yet. Customers can place an order directly and track it with the Order ID.</p>");
});
$$("[data-modal-link]").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
const content = {
terms: ["Terms of Service", "<p>Orders are processed after manual wallet verification. Wrong game IDs, wrong products, or mismatched payments may be rejected.</p>"],
privacy: ["Privacy Policy", "<p>Only order details, payment method, uploaded screenshot, and transaction notes are stored for verification and support.</p>"],
refund: ["Refund Policy", "<p>Rejected or unpaid orders are not processed. Refund requests must include the Order ID and wallet transaction proof.</p>"],
}[link.dataset.modalLink];
if (content) showModal(content[0], content[1]);
});
});
$$("[data-social]").forEach((button) => {
button.addEventListener("click", () => {
showModal(button.dataset.social, `<p>${button.dataset.social} link is not configured yet. Add your real page/link when ready.</p>`);
});
});
$("#modalClose")?.addEventListener("click", closeModal);
$("#modalBackdrop")?.addEventListener("click", (event) => {
if (event.target.id === "modalBackdrop") closeModal();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeModal();
});
}
function setupHomePage() {
$$("[data-game-link]").forEach((card) => {
card.addEventListener("click", (event) => {
event.preventDefault();
window.location.href = `/orders.html?game=${encodeURIComponent(card.dataset.gameLink)}`;
});
});
}
function setupOrdersPage() {
if (!$("#orderForm")) return;
$("#gameSelect").innerHTML = Object.keys(config.menu)
.map((game) => `<option value="${game}">${game}</option>`)
.join("");
const gameParam = new URLSearchParams(window.location.search).get("game");
if (gameParam && config.menu[gameParam]) $("#gameSelect").value = gameParam;
renderProducts();
$("#buyButton").disabled = false;
$("#buyButton").textContent = "Buy";
$("#gameSelect").addEventListener("change", renderProducts);
$("#productSelect").addEventListener("change", renderProductPreview);
if ($("#kbzpay")) $("#kbzpay").textContent = config.kbzpay_number;
if ($("#ayapay")) $("#ayapay").textContent = config.ayapay_number;
if ($("#accountName")) $("#accountName").textContent = config.account_name;
if ($("#ayaQrImage") && config.ayapay_qr_path) {
$("#ayaQrImage").src = config.ayapay_qr_path;
$("#ayaQrPanel").hidden = false;
$("#ayaQrImage").addEventListener("error", () => {
$("#ayaQrPanel").hidden = true;
});
}
$("#orderForm").addEventListener("submit", async (event) => {
event.preventDefault();
const payload = Object.fromEntries(new FormData(event.currentTarget).entries());
try {
const order = await api("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
activeOrderId = order.order_id;
$("#paymentPanel").hidden = false;
$("#orderId").textContent = order.order_id;
$("#amount").textContent = money(order.amount);
setStatus("pending");
startTimer(Math.floor(Date.now() / 1000) + config.expiry_seconds);
$("#paymentPanel").scrollIntoView({ behavior: "smooth", block: "start" });
} catch (error) {
alert(error.message);
}
});
$("#paymentForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
if (!activeOrderId) return;
try {
const { order } = await api(`/api/orders/${encodeURIComponent(activeOrderId)}/payment`, {
method: "POST",
body: new FormData(event.currentTarget),
});
setStatus(order.status);
showModal("Payment Uploaded", "<p>Screenshot uploaded. Please wait for manual wallet verification.</p>");
} catch (error) {
alert(error.message);
}
});
}
function setupTrackPage() {
$("#trackForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
const orderId = new FormData(event.currentTarget).get("track_order_id");
try {
const { order } = await api(`/api/orders/${encodeURIComponent(orderId)}`);
renderTrack(order, $("#trackResult"));
} catch (error) {
$("#trackResult").innerHTML = `<div class="notice">${error.message}</div>`;
}
});
}
function setupAdmin() {
$("#adminForm")?.addEventListener("submit", async (event) => {
event.preventDefault();
const token = new FormData(event.currentTarget).get("token");
try {
const { orders } = await api(`/api/admin/orders?token=${encodeURIComponent(token)}`);
$("#adminOrders").innerHTML = orders.length
? orders.map((order) => renderAdminOrder(order, token)).join("")
: `<div class="notice">No orders yet.</div>`;
} catch (error) {
$("#adminOrders").innerHTML = `<div class="notice">${error.message}</div>`;
}
});
$("#adminOrders")?.addEventListener("click", async (event) => {
const button = event.target.closest("[data-admin-action]");
if (!button) return;
const { adminAction, orderId, token } = button.dataset;
try {
await api(`/api/admin/orders/${orderId}/${adminAction}?token=${encodeURIComponent(token)}`, {
method: "POST",
});
$("#adminForm").requestSubmit ? $("#adminForm").requestSubmit() : $("#adminForm").dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }));
} catch (error) {
alert(error.message);
}
});
}
async function init() {
setupSharedControls();
setupHomePage();
try {
config = await api("/api/config");
setupOrdersPage();
setupTrackPage();
setupAdmin();
renderPricingPage();
} catch (error) {
if ($("#buyButton")) {
$("#buyButton").disabled = true;
$("#buyButton").textContent = "Menu unavailable";
}
showModal("Menu unavailable", `<p>${error.message}</p>`);
}
}
init();
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 440" role="img" aria-label="Mobile Legends card artwork">
<defs>
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#191d63"/>
<stop offset=".52" stop-color="#2b2c86"/>
<stop offset="1" stop-color="#312e81"/>
</linearGradient>
<linearGradient id="crystal" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#bdeaff"/>
<stop offset=".44" stop-color="#38bdf8"/>
<stop offset="1" stop-color="#1e40af"/>
</linearGradient>
<filter id="glow"><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>
<rect width="760" height="440" fill="url(#bg)"/>
<circle cx="620" cy="210" r="170" fill="#93c5fd" opacity=".16"/>
<polygon points="585,90 650,58 690,142 635,230 575,178" fill="url(#crystal)" opacity=".85"/>
<polygon points="405,278 455,296 472,390 416,372 372,318" fill="url(#crystal)" opacity=".92"/>
<path d="M350 440c105-75 242-128 410-178v65c-121 44-238 82-350 113z" fill="#60a5fa" opacity=".78" filter="url(#glow)"/>
<path d="M330 440c94-78 204-132 330-166" stroke="#bae6fd" stroke-width="28" opacity=".9"/>
<text x="70" y="88" fill="#ffe7a6" font-family="Arial Black, Impact, sans-serif" font-size="48" font-weight="900">MOBILE</text>
<text x="72" y="132" fill="#ffe7a6" font-family="Arial Black, Impact, sans-serif" font-size="32" font-weight="900">LEGENDS</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 440" role="img" aria-label="PUBG Mobile card artwork">
<defs>
<radialGradient id="fire" cx="72%" cy="70%" r="45%">
<stop offset="0" stop-color="#ffc400"/>
<stop offset=".42" stop-color="#e66b1f"/>
<stop offset=".78" stop-color="#111827" stop-opacity=".2"/>
<stop offset="1" stop-color="#111827" stop-opacity="0"/>
</radialGradient>
<linearGradient id="helmet" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#cbd5e1"/>
<stop offset=".5" stop-color="#5b6574"/>
<stop offset="1" stop-color="#171d28"/>
</linearGradient>
<filter id="blur"><feGaussianBlur stdDeviation="18"/></filter>
</defs>
<rect width="760" height="440" fill="#111827"/>
<circle cx="560" cy="315" r="210" fill="url(#fire)" filter="url(#blur)"/>
<g transform="translate(470 92)">
<path d="M48 11c62-26 133 18 135 86 2 51-25 91-70 102-45 12-101 2-124-33-29-44-17-131 59-155z" fill="url(#helmet)"/>
<path d="M22 105h128c17 0 28 12 25 28-3 16-15 24-35 24H46c-24 0-38-11-40-27-2-14 4-23 16-25z" fill="#121826"/>
<path d="M58 23c-40 18-60 55-59 96 18 10 83 11 127-1 36-10 61-29 64-55-21-39-76-62-132-40z" fill="#ffffff" opacity=".12"/>
</g>
<g transform="translate(70 62)" fill="none" stroke="#fff" stroke-width="7">
<path d="M0 0h165v118H0z"/>
<path d="M18 18h129v82H18z" opacity=".28"/>
</g>
<text x="95" y="116" fill="#fff" font-family="Arial Black, Impact, sans-serif" font-size="46" font-weight="900">PUBG</text>
<text x="96" y="166" fill="#fff" font-family="Arial Black, Impact, sans-serif" font-size="28" font-weight="900">MOBILE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>How to Topup - Game Topup</title>
<link rel="stylesheet" href="/styles.css?v=20260428-10">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home"><span class="logo-mark">🎮</span><span><strong>GAME TOPUP</strong><small>FAST • SAFE • TRUSTED</small></span></a>
<nav class="main-nav" aria-label="Main navigation">
<a href="/">Home</a><a href="/orders.html">Orders</a><a href="/pricing.html">Pricing</a><a class="active" href="/how-to-topup.html">How to Topup</a><a href="/track.html">Track Order</a>
</nav>
<div class="header-actions">
<button class="currency-button" type="button" id="currencyButton" aria-expanded="false"><span>🇲🇲</span><span>MMK</span><span></span></button>
<div class="currency-menu" id="currencyMenu" hidden><button type="button" data-currency="MMK">🇲🇲 MMK</button><button type="button" data-currency="USD">🇺🇸 USD</button></div>
<button class="login-button" type="button" id="loginButton">Login / Register</button>
</div>
</header>
<main class="page-main narrow-main">
<section class="page-title"><h1>HOW TO <span>TOPUP</span></h1><p>Follow these steps to complete your order correctly.</p></section>
<section class="panel help-panel single-panel">
<h2>Topup Steps</h2>
<div class="steps">
<span>1. Go to Orders and choose PUBG Mobile or Mobile Legends.</span>
<span>2. Enter your user game ID exactly as shown in game.</span>
<span>3. Select the product and click Buy.</span>
<span>4. Pay the exact amount shown, including the unique digits.</span>
<span>5. Upload a clear payment screenshot and optional transaction ID.</span>
<span>6. Wait while payment is manually verified in KBZPay or AYA Pay.</span>
<span>7. When completed, check your order status on the Track Order page.</span>
</div>
</section>
</main>
<div class="modal-backdrop" id="modalBackdrop" hidden><section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button><h2 id="modalTitle">Notice</h2><div id="modalBody"></div></section></div>
<script src="/app.js?v=20260428-10"></script>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Game Topup</title>
<meta http-equiv="Cache-Control" content="no-store">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="/styles.css?v=20260428-13">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home">
<span class="logo-mark">🎮</span>
<span>
<strong>GAME TOPUP</strong>
<small>FAST • SAFE • TRUSTED</small>
</span>
</a>
<nav class="main-nav" aria-label="Main navigation">
<a class="active" href="/">Home</a>
<a href="/orders.html">Orders</a>
<a href="/pricing.html">Pricing</a>
<a href="/how-to-topup.html">How to Topup</a>
<a href="/track.html">Track Order</a>
</nav>
<div class="header-actions">
<button class="currency-button" type="button" id="currencyButton" aria-expanded="false">
<span>🇲🇲</span>
<span>MMK</span>
<span></span>
</button>
<div class="currency-menu" id="currencyMenu" hidden>
<button type="button" data-currency="MMK">🇲🇲 MMK</button>
<button type="button" data-currency="USD">🇺🇸 USD</button>
</div>
<button class="login-button" type="button" id="loginButton">Login / Register</button>
</div>
</header>
<main id="home">
<section class="hero">
<div class="hero-copy">
<h1>CHOOSE <span>YOUR</span> GAME</h1>
<p>Top up your favorite game instantly and securely</p>
</div>
<div class="game-grid" aria-label="Choose game">
<a class="game-card pubg-card" href="/orders.html?game=PUBG%20Mobile" data-game-link="PUBG Mobile">
<img class="game-card-image" src="/assets/pubg-card.png" alt="PUBG Mobile top up">
</a>
<a class="game-card ml-card" href="/orders.html?game=Mobile%20Legends" data-game-link="Mobile Legends">
<img class="game-card-image" src="/assets/ml-card.png" alt="Mobile Legends top up">
</a>
</div>
<div class="trust-strip">
<div>
<span class="trust-icon"></span>
<strong>100% SAFE</strong>
<p>Your payment and account are fully secured.</p>
</div>
<div>
<span class="trust-icon"></span>
<strong>INSTANT DELIVERY</strong>
<p>Top up instantly after payment confirmed.</p>
</div>
<div>
<span class="trust-icon"></span>
<strong>24/7 SUPPORT</strong>
<p>Our support team is always ready to help you.</p>
</div>
<div>
<span class="trust-icon"></span>
<strong>TRUSTED SERVICE</strong>
<p>Thousands of players trust our service.</p>
</div>
</div>
</section>
</main>
<footer class="site-footer" id="support">
<span>© 2024 Game Topup. All rights reserved.</span>
<div>
<a href="#" data-modal-link="terms">Terms of Service</a>
<a href="#" data-modal-link="privacy">Privacy Policy</a>
<a href="#" data-modal-link="refund">Refund Policy</a>
</div>
<span class="socials">
Follow us:
<button type="button" data-social="Telegram"></button>
<button type="button" data-social="Facebook"></button>
<button type="button" data-social="Messenger"></button>
</span>
</footer>
<div class="modal-backdrop" id="modalBackdrop" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button>
<h2 id="modalTitle">Notice</h2>
<div id="modalBody"></div>
</section>
</div>
<script src="/app.js?v=20260428-13"></script>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orders - Game Topup</title>
<meta http-equiv="Cache-Control" content="no-store">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="/styles.css?v=20260428-11">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home">
<span class="logo-mark">🎮</span>
<span><strong>GAME TOPUP</strong><small>FAST • SAFE • TRUSTED</small></span>
</a>
<nav class="main-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a class="active" href="/orders.html">Orders</a>
<a href="/pricing.html">Pricing</a>
<a href="/how-to-topup.html">How to Topup</a>
<a href="/track.html">Track Order</a>
</nav>
<div class="header-actions">
<button class="currency-button" type="button" id="currencyButton" aria-expanded="false"><span>🇲🇲</span><span>MMK</span><span></span></button>
<div class="currency-menu" id="currencyMenu" hidden>
<button type="button" data-currency="MMK">🇲🇲 MMK</button>
<button type="button" data-currency="USD">🇺🇸 USD</button>
</div>
<button class="login-button" type="button" id="loginButton">Login / Register</button>
</div>
</header>
<main class="page-main">
<section class="page-title">
<h1>PLACE <span>ORDER</span></h1>
<p>Select game, product, and upload payment proof after checkout.</p>
</section>
<section class="shop-layout order-layout" id="pricing">
<section class="panel order-panel" id="orderSection">
<div class="section-heading">
<span>Selected game</span>
<h2 id="selectedGameTitle">Choose Product</h2>
</div>
<form id="orderForm" class="stack">
<label>
Game
<select name="game" id="gameSelect" required></select>
</label>
<label>
User Game ID
<input name="user_game_id" placeholder="Enter user ID / server ID" required>
</label>
<label>
Product
<select name="product" id="productSelect" required></select>
</label>
<div class="product-preview" id="productPreview">Select a product to see price.</div>
<button id="buyButton" type="submit" disabled>Loading menu...</button>
</form>
</section>
<section class="panel payment-panel" id="paymentPanel" hidden>
<div class="status-row">
<div><span class="muted">Order ID</span><strong id="orderId">-</strong></div>
<span class="badge" id="statusBadge">pending</span>
</div>
<div class="amount-box"><span>Exact amount</span><strong id="amount">0 MMK</strong></div>
<div class="payment-grid">
<div><span>KBZPay</span><strong id="kbzpay">-</strong></div>
<div><span>AYA Pay</span><strong id="ayapay">-</strong></div>
<div><span>Account name</span><strong id="accountName">-</strong></div>
<div><span>Time left</span><strong id="timer">10:00</strong></div>
</div>
<div class="qr-panel" id="ayaQrPanel" hidden>
<div><span>AYA Pay QR</span><strong>Scan to pay</strong></div>
<img id="ayaQrImage" alt="AYA Pay banking QR code">
</div>
<ul class="instructions">
<li>Pay the exact amount shown above.</li>
<li>Upload screenshot after payment.</li>
<li>Payment must be completed within 10 minutes.</li>
<li>Screenshots are only evidence. Wallet verification decides approval.</li>
</ul>
<form id="paymentForm" class="stack">
<label>Payment method<select name="payment_method" required><option>KBZPay</option><option>AYA Pay</option></select></label>
<label>Transaction ID<input name="transaction_id" placeholder="Optional"></label>
<label>Payment screenshot<input name="screenshot" type="file" accept="image/png,image/jpeg,image/webp" required></label>
<button type="submit">Upload Screenshot</button>
</form>
</section>
</section>
</main>
<div class="modal-backdrop" id="modalBackdrop" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button>
<h2 id="modalTitle">Notice</h2>
<div id="modalBody"></div>
</section>
</div>
<script src="/app.js?v=20260428-11"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pricing - Game Topup</title>
<link rel="stylesheet" href="/styles.css?v=20260428-10">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home"><span class="logo-mark">🎮</span><span><strong>GAME TOPUP</strong><small>FAST • SAFE • TRUSTED</small></span></a>
<nav class="main-nav" aria-label="Main navigation">
<a href="/">Home</a><a href="/orders.html">Orders</a><a class="active" href="/pricing.html">Pricing</a><a href="/how-to-topup.html">How to Topup</a><a href="/track.html">Track Order</a>
</nav>
<div class="header-actions">
<button class="currency-button" type="button" id="currencyButton" aria-expanded="false"><span>🇲🇲</span><span>MMK</span><span></span></button>
<div class="currency-menu" id="currencyMenu" hidden><button type="button" data-currency="MMK">🇲🇲 MMK</button><button type="button" data-currency="USD">🇺🇸 USD</button></div>
<button class="login-button" type="button" id="loginButton">Login / Register</button>
</div>
</header>
<main class="page-main">
<section class="page-title"><h1>PRODUCT <span>PRICING</span></h1><p>Base prices before unique payment digits are added at checkout.</p></section>
<section class="pricing-grid" id="pricingGrid"></section>
</main>
<div class="modal-backdrop" id="modalBackdrop" hidden><section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button><h2 id="modalTitle">Notice</h2><div id="modalBody"></div></section></div>
<script src="/app.js?v=20260428-10"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Track Order - Game Topup</title>
<link rel="stylesheet" href="/styles.css?v=20260428-10">
</head>
<body>
<header class="site-header">
<a class="logo" href="/" aria-label="Game Topup home"><span class="logo-mark">🎮</span><span><strong>GAME TOPUP</strong><small>FAST • SAFE • TRUSTED</small></span></a>
<nav class="main-nav" aria-label="Main navigation">
<a href="/">Home</a><a href="/orders.html">Orders</a><a href="/pricing.html">Pricing</a><a href="/how-to-topup.html">How to Topup</a><a class="active" href="/track.html">Track Order</a>
</nav>
<div class="header-actions">
<button class="currency-button" type="button" id="currencyButton" aria-expanded="false"><span>🇲🇲</span><span>MMK</span><span></span></button>
<div class="currency-menu" id="currencyMenu" hidden><button type="button" data-currency="MMK">🇲🇲 MMK</button><button type="button" data-currency="USD">🇺🇸 USD</button></div>
<button class="login-button" type="button" id="loginButton">Login / Register</button>
</div>
</header>
<main class="page-main narrow-main">
<section class="page-title"><h1>TRACK <span>ORDER</span></h1><p>Enter your order ID to see payment and delivery status.</p></section>
<section class="panel tracking-panel single-panel">
<h2>Track Order</h2>
<form id="trackForm" class="inline-form">
<input name="track_order_id" placeholder="ORD123456" required>
<button type="submit">Check</button>
</form>
<div id="trackResult" class="track-result"></div>
</section>
</main>
<div class="modal-backdrop" id="modalBackdrop" hidden><section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><button class="modal-close" id="modalClose" type="button" aria-label="Close">×</button><h2 id="modalTitle">Notice</h2><div id="modalBody"></div></section></div>
<script src="/app.js?v=20260428-10"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
LIVE_CHECK_DRY_RUN=true
LIVE_CHECK_INTERVAL_SECONDS=60
SMARTCOIN_HEALTH_URL=http://127.0.0.1:8000/api/config
SMARTCOIN_HEALTH_TIMEOUT_MS=5000
SMILE_BALANCE_SOURCE=env
SMILE_BALANCE=100000
SMILE_BALANCE_CURRENCY=MMK
SMILE_WARNING_BALANCE=50000
SMILE_CRITICAL_BALANCE=10000
QUEUE_STATUS_FILE=./state/queue-status.json
BROWSER_HEARTBEAT_FILE=./state/browser-heartbeat.json
SESSION_STATUS_FILE=./state/session-status.json
BROWSER_HEARTBEAT_MAX_AGE_SECONDS=120
+54
View File
@@ -0,0 +1,54 @@
# Live Checking System
Monitors the SmartCoin automation environment and triggers notification emails when important checks fail.
## Checks
- SmartCoin app health URL
- Smile.one balance threshold
- queue status file
- browser heartbeat file
- Smile.one session status file
## Commands
```bash
npm run check
npm start
```
`npm run check` runs one pass. `npm start` runs continuously.
## Dry Run
`LIVE_CHECK_DRY_RUN=true` logs alerts without sending email. Set it to `false` after `notification/.env` has real Gmail credentials.
## State File Examples
`state/queue-status.json`
```json
{
"ok": true,
"pending": 0,
"failed": 0
}
```
`state/browser-heartbeat.json`
```json
{
"ok": true,
"timestamp": "2026-05-06T10:00:00.000Z"
}
```
`state/session-status.json`
```json
{
"valid": true,
"account": "Smile.one main account"
}
```
@@ -0,0 +1,50 @@
async function checkSmileBalance(config) {
const balance = config.smileBalance;
const currency = config.smileBalanceCurrency;
if (balance <= config.smileCriticalBalance) {
return {
ok: false,
type: "lowBalance",
code: "smile_balance_critical",
message: `Smile.one balance is critical: ${balance.toLocaleString()} ${currency}.`,
details: {
balance,
currency,
level: "critical",
threshold: config.smileCriticalBalance,
source: config.smileBalanceSource,
},
};
}
if (balance <= config.smileWarningBalance) {
return {
ok: false,
type: "lowBalance",
code: "smile_balance_warning",
message: `Smile.one balance is low: ${balance.toLocaleString()} ${currency}.`,
details: {
balance,
currency,
level: "warning",
threshold: config.smileWarningBalance,
source: config.smileBalanceSource,
},
};
}
return {
ok: true,
code: "smile_balance_ok",
details: {
balance,
currency,
source: config.smileBalanceSource,
},
};
}
module.exports = {
checkSmileBalance,
};
@@ -0,0 +1,62 @@
const http = require("http");
const https = require("https");
function request(url, timeoutMs) {
return new Promise((resolve, reject) => {
const client = url.startsWith("https:") ? https : http;
const req = client.get(url, { timeout: timeoutMs }, (res) => {
res.resume();
resolve({
statusCode: res.statusCode,
ok: res.statusCode >= 200 && res.statusCode < 300,
});
});
req.on("timeout", () => {
req.destroy(new Error(`Health check timed out after ${timeoutMs}ms`));
});
req.on("error", reject);
});
}
async function checkHttpHealth(config) {
try {
const result = await request(config.healthUrl, config.healthTimeoutMs);
if (!result.ok) {
return {
ok: false,
type: "critical",
code: "smartcoin_health_bad_status",
message: `SmartCoin health URL returned HTTP ${result.statusCode}.`,
details: {
url: config.healthUrl,
statusCode: result.statusCode,
},
};
}
return {
ok: true,
code: "smartcoin_health_ok",
details: {
url: config.healthUrl,
statusCode: result.statusCode,
},
};
} catch (error) {
return {
ok: false,
type: "critical",
code: "smartcoin_health_failed",
message: `SmartCoin health check failed: ${error.message}`,
details: {
url: config.healthUrl,
error: error.message,
},
};
}
}
module.exports = {
checkHttpHealth,
};
@@ -0,0 +1,131 @@
const fs = require("fs");
function readJson(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
async function checkQueueStatus(config) {
const status = readJson(config.queueStatusFile);
if (!status) {
return {
ok: true,
code: "queue_status_missing",
details: {
file: config.queueStatusFile,
note: "Queue status file does not exist yet.",
},
};
}
if (status.ok === false || Number(status.failed || 0) > 0) {
return {
ok: false,
type: "critical",
code: "queue_error",
message: status.message || "Queue system reported an error.",
details: {
file: config.queueStatusFile,
...status,
},
};
}
return {
ok: true,
code: "queue_ok",
details: {
file: config.queueStatusFile,
...status,
},
};
}
async function checkBrowserHeartbeat(config) {
const heartbeat = readJson(config.browserHeartbeatFile);
if (!heartbeat) {
return {
ok: true,
code: "browser_heartbeat_missing",
details: {
file: config.browserHeartbeatFile,
note: "Browser heartbeat file does not exist yet.",
},
};
}
const timestamp = Date.parse(heartbeat.timestamp || "");
const ageSeconds = Number.isFinite(timestamp) ? Math.round((Date.now() - timestamp) / 1000) : Infinity;
const stale = ageSeconds > config.browserHeartbeatMaxAgeSeconds;
if (heartbeat.ok === false || stale) {
return {
ok: false,
type: "critical",
code: stale ? "browser_heartbeat_stale" : "browser_crashed",
message: stale
? `Browser heartbeat is stale: ${ageSeconds}s old.`
: heartbeat.message || "Browser reported a crash.",
details: {
file: config.browserHeartbeatFile,
ageSeconds,
maxAgeSeconds: config.browserHeartbeatMaxAgeSeconds,
...heartbeat,
},
};
}
return {
ok: true,
code: "browser_heartbeat_ok",
details: {
file: config.browserHeartbeatFile,
ageSeconds,
...heartbeat,
},
};
}
async function checkSessionStatus(config) {
const session = readJson(config.sessionStatusFile);
if (!session) {
return {
ok: true,
code: "session_status_missing",
details: {
file: config.sessionStatusFile,
note: "Session status file does not exist yet.",
},
};
}
if (session.valid === false || session.expired === true) {
return {
ok: false,
type: "sessionExpired",
code: "session_expired",
message: session.message || "Smile.one account session expired.",
details: {
file: config.sessionStatusFile,
...session,
},
};
}
return {
ok: true,
code: "session_ok",
details: {
file: config.sessionStatusFile,
...session,
},
};
}
module.exports = {
checkQueueStatus,
checkBrowserHeartbeat,
checkSessionStatus,
};
+42
View File
@@ -0,0 +1,42 @@
const path = require("path");
function boolFromEnv(name, fallback) {
const raw = process.env[name];
if (raw === undefined) return fallback;
return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
}
function numberFromEnv(name, fallback) {
const raw = process.env[name];
if (!raw) return fallback;
const value = Number(raw);
return Number.isFinite(value) ? value : fallback;
}
function pathFromEnv(name, fallback) {
const raw = process.env[name] || fallback;
if (path.isAbsolute(raw)) return raw;
return path.join(__dirname, raw);
}
function getConfig() {
return {
dryRun: boolFromEnv("LIVE_CHECK_DRY_RUN", true),
intervalSeconds: numberFromEnv("LIVE_CHECK_INTERVAL_SECONDS", 60),
healthUrl: process.env.SMARTCOIN_HEALTH_URL || "http://127.0.0.1:8000/api/config",
healthTimeoutMs: numberFromEnv("SMARTCOIN_HEALTH_TIMEOUT_MS", 5000),
smileBalanceSource: process.env.SMILE_BALANCE_SOURCE || "env",
smileBalance: numberFromEnv("SMILE_BALANCE", 100000),
smileBalanceCurrency: process.env.SMILE_BALANCE_CURRENCY || "MMK",
smileWarningBalance: numberFromEnv("SMILE_WARNING_BALANCE", 50000),
smileCriticalBalance: numberFromEnv("SMILE_CRITICAL_BALANCE", 10000),
queueStatusFile: pathFromEnv("QUEUE_STATUS_FILE", "./state/queue-status.json"),
browserHeartbeatFile: pathFromEnv("BROWSER_HEARTBEAT_FILE", "./state/browser-heartbeat.json"),
sessionStatusFile: pathFromEnv("SESSION_STATUS_FILE", "./state/session-status.json"),
browserHeartbeatMaxAgeSeconds: numberFromEnv("BROWSER_HEARTBEAT_MAX_AGE_SECONDS", 120),
};
}
module.exports = {
getConfig,
};
+11
View File
@@ -0,0 +1,11 @@
{
"name": "smartcoin-live-checking",
"version": "1.0.0",
"private": true,
"description": "Live checking and monitoring system for SmartCoin automation health.",
"main": "server.js",
"scripts": {
"start": "node server.js monitor",
"check": "node server.js once"
}
}
+47
View File
@@ -0,0 +1,47 @@
const { getConfig } = require("./config");
const { runChecks } = require("./services/liveChecker");
const { loadEnv } = require("./utils/env");
const logger = require("./utils/logger");
loadEnv();
async function runOnce() {
const config = getConfig();
const summary = await runChecks(config);
console.log(JSON.stringify(summary, null, 2));
return summary;
}
async function monitor() {
const config = getConfig();
logger.info("Live checking monitor started", {
intervalSeconds: config.intervalSeconds,
dryRun: config.dryRun,
});
while (true) {
try {
await runChecks(getConfig());
} catch (error) {
logger.error("Live checking cycle failed", {
error: error.message,
stack: error.stack,
});
}
await new Promise((resolve) => setTimeout(resolve, config.intervalSeconds * 1000));
}
}
if (require.main === module) {
const command = process.argv[2] || "once";
const runner = command === "monitor" ? monitor : runOnce;
runner().catch((error) => {
logger.error("Live checking failed", {
error: error.message,
stack: error.stack,
});
process.exit(1);
});
}
@@ -0,0 +1,80 @@
const {
sendAutomationFailureAlert,
sendCriticalErrorAlert,
sendLowBalanceAlert,
sendSessionExpiredAlert,
} = require("../../notification/services/emailService");
const { checkSmileBalance } = require("../checks/balanceCheck");
const { checkHttpHealth } = require("../checks/httpHealthCheck");
const {
checkBrowserHeartbeat,
checkQueueStatus,
checkSessionStatus,
} = require("../checks/stateFileCheck");
const logger = require("../utils/logger");
async function notify(result, config) {
if (result.ok) return { sent: false, skipped: true, reason: "ok" };
if (config.dryRun) {
logger.warn("Dry-run alert", {
alertType: result.type,
code: result.code,
message: result.message,
details: result.details,
});
return { sent: false, skipped: true, reason: "dry_run" };
}
if (result.type === "lowBalance") {
return sendLowBalanceAlert(result.details.balance, result.details.currency);
}
if (result.type === "sessionExpired") {
return sendSessionExpiredAlert(result.details.account, result.details);
}
if (result.type === "automationFailure") {
return sendAutomationFailureAlert(result.message, result.details);
}
return sendCriticalErrorAlert(result.message, {
component: result.code,
...result.details,
});
}
async function runChecks(config) {
const checks = [
checkHttpHealth,
checkSmileBalance,
checkQueueStatus,
checkBrowserHeartbeat,
checkSessionStatus,
];
const results = [];
for (const check of checks) {
const result = await check(config);
results.push(result);
logger.info("Live check completed", {
code: result.code,
ok: result.ok,
details: result.details,
});
if (!result.ok) {
await notify(result, config);
}
}
return {
ok: results.every((result) => result.ok),
checkedAt: new Date().toISOString(),
results,
};
}
module.exports = {
runChecks,
};
+38
View File
@@ -0,0 +1,38 @@
const fs = require("fs");
const path = require("path");
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
const content = fs.readFileSync(filePath, "utf8");
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const index = trimmed.indexOf("=");
if (index === -1) continue;
const key = trimmed.slice(0, index).trim();
let value = trimmed.slice(index + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (!process.env[key]) {
process.env[key] = value;
}
}
}
function loadEnv() {
const root = path.join(__dirname, "..", "..");
loadEnvFile(path.join(root, "notification", ".env"));
loadEnvFile(path.join(root, "live-checking", ".env"));
}
module.exports = {
loadEnv,
};
+36
View File
@@ -0,0 +1,36 @@
const fs = require("fs");
const path = require("path");
const logDir = path.join(__dirname, "..", "logs");
const logFile = path.join(logDir, "live-checking.log");
function ensureLogDir() {
fs.mkdirSync(logDir, { recursive: true });
}
function write(level, message, meta = {}) {
ensureLogDir();
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...meta,
};
const line = JSON.stringify(entry);
fs.appendFileSync(logFile, `${line}\n`);
const printer = level === "error" ? console.error : console.log;
printer(line);
}
module.exports = {
info(message, meta) {
write("info", message, meta);
},
warn(message, meta) {
write("warn", message, meta);
},
error(message, meta) {
write("error", message, meta);
},
};
+1
View File
@@ -0,0 +1 @@
+14
View File
@@ -0,0 +1,14 @@
{"timestamp":"2026-05-06T10:37:49.688Z","level":"error","message":"Automation worker failed","error":"fetch failed","stack":"TypeError: fetch failed"}
{"timestamp":"2026-05-06T10:38:40.816Z","level":"info","message":"No waiting automation jobs","failed":0}
{"timestamp":"2026-05-06T10:41:31.008Z","level":"error","message":"Automation worker failed","error":"fetch failed","stack":"TypeError: fetch failed"}
{"timestamp":"2026-05-10T14:03:57.475Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":false,"userDataDir":"/Users/ven/Projects/SmartCoin/backend/sessions/smoke-check"}
{"timestamp":"2026-05-10T14:03:58.536Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"}
{"timestamp":"2026-05-11T07:27:36.361Z","level":"error","message":"Automation worker failed","error":"Cannot reach SmartCoin API at http://127.0.0.1:8000: connect EPERM 127.0.0.1:8000 - Local (0.0.0.0:0)","stack":"Error: Cannot reach SmartCoin API at http://127.0.0.1:8000: connect EPERM 127.0.0.1:8000 - Local (0.0.0.0:0)\n at request (/Users/ven/SmartCoin/SmartCoin/backend/services/apiClient.js:23:11)\n at process.processTicksAndRejections (node:internal/process/task_queues:104:5)\n at async Promise.all (index 0)\n at async processNextOrder (/Users/ven/SmartCoin/SmartCoin/backend/workers/queueWorker.js:60:37)"}
{"timestamp":"2026-05-11T07:28:07.493Z","level":"info","message":"No waiting automation jobs","failed":0}
{"timestamp":"2026-05-11T07:28:15.176Z","level":"error","message":"Puppeteer smoke check failed","error":"Failed to launch the browser process: Code: null\n\nstderr:\nchrome_crashpad_handler: --database is required\nTry 'chrome_crashpad_handler --help' for more information.\n[0511/142815.143790:ERROR:third_party/crashpad/crashpad/util/file/file_io.cc:103] ReadExactly: expected 8, observed 0\n[0511/142815.148765:ERROR:third_party/crashpad/crashpad/client/crash_report_database_mac.mm:109] mkdir : No such file or directory (2)\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n","stack":"Error: Failed to launch the browser process: Code: null\n\nstderr:\nchrome_crashpad_handler: --database is required\nTry 'chrome_crashpad_handler --help' for more information.\n[0511/142815.143790:ERROR:third_party/crashpad/crashpad/util/file/file_io.cc:103] ReadExactly: expected 8, observed 0\n[0511/142815.148765:ERROR:third_party/crashpad/crashpad/client/crash_report_database_mac.mm:109] mkdir : No such file or directory (2)\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n\n at ChildProcess.onClose (/Users/ven/SmartCoin/SmartCoin/backend/node_modules/@puppeteer/browsers/lib/cjs/launch.js:350:24)\n at ChildProcess.emit (node:events:521:24)\n at ChildProcess._handle.onexit (node:internal/child_process:294:12)"}
{"timestamp":"2026-05-11T07:28:29.611Z","level":"error","message":"Puppeteer smoke check failed","error":"Failed to launch the browser process: Code: null\n\nstderr:\n\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n","stack":"Error: Failed to launch the browser process: Code: null\n\nstderr:\n\n\nTROUBLESHOOTING: https://pptr.dev/troubleshooting\n\n at ChildProcess.onClose (/Users/ven/SmartCoin/SmartCoin/backend/node_modules/@puppeteer/browsers/lib/cjs/launch.js:350:24)\n at ChildProcess.emit (node:events:521:24)\n at ChildProcess._handle.onexit (node:internal/child_process:294:12)"}
{"timestamp":"2026-05-11T07:28:42.816Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":true,"userDataDir":"/Users/ven/SmartCoin/SmartCoin/backend/sessions/smoke-check"}
{"timestamp":"2026-05-11T07:28:44.547Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"}
{"timestamp":"2026-05-11T07:29:22.661Z","level":"info","message":"Puppeteer browser launched","accountEmail":"smoke-check","headless":false,"userDataDir":"/Users/ven/SmartCoin/SmartCoin/backend/sessions/smoke-check"}
{"timestamp":"2026-05-11T07:29:24.429Z","level":"info","message":"Puppeteer smoke check loaded Smile.one","url":"https://www.smile.one/","title":"Smile.One - (Brazil) | Créditos de Jogos e Serviços"}
{"timestamp":"2026-05-11T07:55:54.136Z","level":"info","message":"No waiting automation jobs","failed":0}
+7
View File
@@ -0,0 +1,7 @@
EMAIL_USER=yourgmail@gmail.com
EMAIL_PASS=your_app_password
ADMIN_EMAIL=yourgmail@gmail.com
EMAIL_WARNING_BALANCE=50000
EMAIL_CRITICAL_BALANCE=10000
EMAIL_COOLDOWN_MINUTES=30
+37
View File
@@ -0,0 +1,37 @@
{
"name": "smartcoin-email-notifications",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "smartcoin-email-notifications",
"version": "1.0.0",
"dependencies": {
"dotenv": "^16.4.7",
"nodemailer": "^8.0.7"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/nodemailer": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
"integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "smartcoin-email-notifications",
"version": "1.0.0",
"private": true,
"description": "Email notification module for SmartCoin game top-up automation alerts.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test:email": "node server.js test"
},
"dependencies": {
"dotenv": "^16.4.7",
"nodemailer": "^8.0.7"
}
}
+49
View File
@@ -0,0 +1,49 @@
require("dotenv").config();
const {
sendLowBalanceAlert,
sendAutomationFailureAlert,
sendCriticalErrorAlert,
sendSessionExpiredAlert,
} = require("./services/emailService");
async function runTest() {
const kind = process.argv[3] || "low-balance";
if (kind === "low-balance") {
return sendLowBalanceAlert(45000, "MMK");
}
if (kind === "automation-failure") {
return sendAutomationFailureAlert("Purchase timeout", {
orderId: "ORD-TEST",
product: "86 Diamonds",
});
}
if (kind === "critical-error") {
return sendCriticalErrorAlert(new Error("Browser crashed"), {
component: "browser",
});
}
if (kind === "session-expired") {
return sendSessionExpiredAlert("Smile.one main account");
}
throw new Error(`Unknown test type: ${kind}`);
}
if (require.main === module) {
if (process.argv[2] !== "test") {
console.log("Email notification module is ready.");
console.log("Run `npm run test:email` after creating notification/.env.");
process.exit(0);
}
runTest()
.then((result) => {
console.log(JSON.stringify(result, null, 2));
})
.catch((error) => {
console.error(error.message);
process.exit(1);
});
}
@@ -0,0 +1,210 @@
const nodemailer = require("nodemailer");
const logger = require("../utils/logger");
const DEFAULT_WARNING_BALANCE = 50000;
const DEFAULT_CRITICAL_BALANCE = 10000;
const DEFAULT_COOLDOWN_MINUTES = 30;
const SUBJECTS = {
lowBalance: "Smile Balance Low",
automationFailure: "Automation Failed",
criticalError: "Critical System Error",
sessionExpired: "Smile Session Expired",
};
const lastSentAt = new Map();
function numberFromEnv(name, fallback) {
const raw = process.env[name];
if (!raw) return fallback;
const value = Number(raw);
return Number.isFinite(value) ? value : fallback;
}
function getConfig() {
return {
emailUser: process.env.EMAIL_USER,
emailPass: process.env.EMAIL_PASS,
adminEmail: process.env.ADMIN_EMAIL,
warningBalance: numberFromEnv("EMAIL_WARNING_BALANCE", DEFAULT_WARNING_BALANCE),
criticalBalance: numberFromEnv("EMAIL_CRITICAL_BALANCE", DEFAULT_CRITICAL_BALANCE),
cooldownMinutes: numberFromEnv("EMAIL_COOLDOWN_MINUTES", DEFAULT_COOLDOWN_MINUTES),
};
}
function validateConfig(config) {
const missing = [];
if (!config.emailUser) missing.push("EMAIL_USER");
if (!config.emailPass) missing.push("EMAIL_PASS");
if (!config.adminEmail) missing.push("ADMIN_EMAIL");
if (missing.length > 0) {
throw new Error(`Missing email environment variables: ${missing.join(", ")}`);
}
}
function createTransporter(config = getConfig()) {
validateConfig(config);
return nodemailer.createTransport({
service: "gmail",
port: 587,
secure: false,
auth: {
user: config.emailUser,
pass: config.emailPass,
},
connectionTimeout: 10000,
greetingTimeout: 10000,
socketTimeout: 15000,
});
}
function cooldownKey(type, key = "default") {
return `${type}:${key}`;
}
function canSend(type, key, cooldownMinutes) {
const now = Date.now();
const previous = lastSentAt.get(cooldownKey(type, key));
if (!previous) return true;
return now - previous >= cooldownMinutes * 60 * 1000;
}
function markSent(type, key) {
lastSentAt.set(cooldownKey(type, key), Date.now());
}
function formatDetails(details = {}) {
return Object.entries(details)
.filter(([, value]) => value !== undefined && value !== null && value !== "")
.map(([key, value]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(String(value))}</td></tr>`)
.join("");
}
function escapeHtml(value) {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function buildEmailHtml(title, message, details) {
const rows = formatDetails(details);
return `
<div style="font-family: Arial, sans-serif; color: #172033; line-height: 1.5;">
<h2 style="margin: 0 0 12px;">${escapeHtml(title)}</h2>
<p>${escapeHtml(message)}</p>
${
rows
? `<table style="border-collapse: collapse; margin-top: 16px;">
${rows}
</table>`
: ""
}
<p style="color: #64748b; font-size: 12px; margin-top: 20px;">
Sent by SmartCoin notification system at ${new Date().toISOString()}.
</p>
</div>
`;
}
async function sendNotification(type, { subject, message, details = {}, cooldownKey: key = "default" }) {
const config = getConfig();
const cooldownMinutes = config.cooldownMinutes;
if (!canSend(type, key, cooldownMinutes)) {
logger.info("Email alert skipped due to cooldown", { alertType: type, cooldownKey: key });
return { sent: false, skipped: true, reason: "cooldown" };
}
const transporter = createTransporter(config);
const title = subject || SUBJECTS[type] || "SmartCoin Alert";
try {
const result = await transporter.sendMail({
from: `"SmartCoin Alerts" <${config.emailUser}>`,
to: config.adminEmail,
subject: title,
text: `${message}\n\n${JSON.stringify(details, null, 2)}`,
html: buildEmailHtml(title, message, details),
});
markSent(type, key);
logger.info("Email alert sent", {
alertType: type,
cooldownKey: key,
messageId: result.messageId,
accepted: result.accepted,
rejected: result.rejected,
});
return { sent: true, messageId: result.messageId };
} catch (error) {
logger.error("Email alert failed", {
alertType: type,
cooldownKey: key,
error: error.message,
});
throw error;
}
}
async function sendLowBalanceAlert(balance, currency = "MMK") {
const config = getConfig();
const threshold = balance <= config.criticalBalance ? config.criticalBalance : config.warningBalance;
const level = balance <= config.criticalBalance ? "critical" : "warning";
if (balance > config.warningBalance) {
return { sent: false, skipped: true, reason: "balance_above_threshold" };
}
return sendNotification("lowBalance", {
message: `Smile.one balance is ${level}: ${balance.toLocaleString()} ${currency}.`,
details: {
balance,
currency,
level,
threshold,
},
cooldownKey: level,
});
}
async function sendAutomationFailureAlert(reason, details = {}) {
return sendNotification("automationFailure", {
message: reason || "Purchase automation failed.",
details,
cooldownKey: details.orderId || details.jobId || reason || "automation",
});
}
async function sendCriticalErrorAlert(error, details = {}) {
const message = error instanceof Error ? error.message : String(error || "Critical system error");
return sendNotification("criticalError", {
message,
details: {
...details,
stack: error instanceof Error ? error.stack : undefined,
},
cooldownKey: details.component || message,
});
}
async function sendSessionExpiredAlert(account, details = {}) {
return sendNotification("sessionExpired", {
message: `Smile.one session expired${account ? ` for ${account}` : ""}.`,
details: {
account,
...details,
},
cooldownKey: account || "smile-session",
});
}
module.exports = {
sendNotification,
sendLowBalanceAlert,
sendAutomationFailureAlert,
sendCriticalErrorAlert,
sendSessionExpiredAlert,
};
+33
View File
@@ -0,0 +1,33 @@
const fs = require("fs");
const path = require("path");
const logDir = path.join(__dirname, "..", "logs");
const logFile = path.join(logDir, "email-notifications.log");
function ensureLogDir() {
fs.mkdirSync(logDir, { recursive: true });
}
function write(level, message, meta = {}) {
ensureLogDir();
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...meta,
};
const line = JSON.stringify(entry);
fs.appendFileSync(logFile, `${line}\n`);
const printer = level === "error" ? console.error : console.log;
printer(line);
}
module.exports = {
info(message, meta) {
write("info", message, meta);
},
error(message, meta) {
write("error", message, meta);
},
};
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
"$SCRIPT_DIR/stop_server.sh"
"$SCRIPT_DIR/start_server.sh"
+324
View File
@@ -0,0 +1,324 @@
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58808)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58809)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58810)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58811)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58812)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58813)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58814)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58815)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58816)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58817)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58818)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 58819)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 59371)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
----------------------------------------
Exception occurred during processing of request from ('127.0.0.1', 59374)
Traceback (most recent call last):
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 683, in process_request_thread
self.finish_request(request, client_address)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 747, in __init__
self.handle()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 434, in handle
self.handle_one_request()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 422, in handle_one_request
method()
File "/Users/ven/Projects/game item resell/server.py", line 302, in do_GET
File "/Users/ven/Projects/game item resell/server.py", line 142, in expire_old_orders
File "/Users/ven/Projects/game item resell/server.py", line 62, in db
sqlite3.OperationalError: unable to open database file
----------------------------------------
Traceback (most recent call last):
File "/Users/ven/Projects/SmartCoin/server.py", line 542, in <module>
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__
self.server_bind()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind
socketserver.TCPServer.server_bind(self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind
self.socket.bind(self.server_address)
OSError: [Errno 48] Address already in use
Traceback (most recent call last):
File "/Users/ven/Projects/SmartCoin/server.py", line 542, in <module>
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__
self.server_bind()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind
socketserver.TCPServer.server_bind(self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind
self.socket.bind(self.server_address)
PermissionError: [Errno 1] Operation not permitted
Traceback (most recent call last):
File "/Users/ven/Projects/SmartCoin/server.py", line 685, in <module>
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__
self.server_bind()
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind
socketserver.TCPServer.server_bind(self)
File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind
self.socket.bind(self.server_address)
PermissionError: [Errno 1] Operation not permitted
Server running at http://127.0.0.1:8000
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
Server running at http://127.0.0.1:8000
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET / HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - "GET / HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET /orders.html?game=PUBG%20Mobile HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-11 HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-11 HTTP/1.1" 200 -
127.0.0.1 - "GET /aya-pay-qr.png HTTP/1.1" 404 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
Server running at http://127.0.0.1:8000
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET / HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET / HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/pubg-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-13 HTTP/1.1" 200 -
127.0.0.1 - "GET /assets/ml-card.png HTTP/1.1" 200 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET /orders.html?game=PUBG%20Mobile HTTP/1.1" 200 -
127.0.0.1 - "GET /styles.css?v=20260428-11 HTTP/1.1" 200 -
127.0.0.1 - "GET /app.js?v=20260428-11 HTTP/1.1" 200 -
127.0.0.1 - "GET /api/config HTTP/1.1" 200 -
127.0.0.1 - "GET /aya-pay-qr.png HTTP/1.1" 404 -
127.0.0.1 - "POST /api/orders HTTP/1.1" 200 -
+1
View File
@@ -0,0 +1 @@
70547
+687
View File
@@ -0,0 +1,687 @@
#!/usr/bin/env python3
import cgi
import json
import os
import random
import secrets
import sqlite3
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
BACKEND_DIR = BASE_DIR / "backend"
DB_DIR = BACKEND_DIR / "database"
DB_PATH = DB_DIR / "orders.db"
UPLOAD_DIR = BACKEND_DIR / "uploads"
PUBLIC_DIR = BASE_DIR / "frontend"
ORDER_EXPIRY_SECONDS = int(os.getenv("ORDER_EXPIRY_SECONDS", "600"))
ACCOUNT_NAME = os.getenv("PAYMENT_ACCOUNT_NAME", "Your Account Name")
KBZPAY_NUMBER = os.getenv("KBZPAY_NUMBER", "09XXXXXXXXX")
AYAPAY_NUMBER = os.getenv("AYAPAY_NUMBER", "09YYYYYYYYY")
AYAPAY_QR_PATH = os.getenv("AYAPAY_QR_PATH", "/aya-pay-qr.png")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "change-me")
PRODUCT_MENU = {
"Mobile Legends": {
"86 Diamonds": 2500,
"172 Diamonds": 5000,
"257 Diamonds": 7500,
"344 Diamonds": 10000,
"429 Diamonds": 12500,
"Weekly Pass": 7000,
"Monthly Pass": 28000,
},
"PUBG Mobile": {
"60 UC": 3200,
"325 UC": 15500,
"660 UC": 31000,
"1800 UC": 82000,
"3850 UC": 165000,
"8100 UC": 330000,
},
}
STATUSES = {
"pending",
"waiting_verification",
"paid",
"rejected",
"completed",
"expired",
}
def db():
conn = sqlite3.connect(DB_PATH, timeout=10)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA busy_timeout = 10000")
return conn
def init_db():
DB_DIR.mkdir(parents=True, exist_ok=True)
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
PUBLIC_DIR.mkdir(exist_ok=True)
try:
with db() as conn:
try:
conn.execute("PRAGMA journal_mode = WAL")
except sqlite3.OperationalError as exc:
if "locked" not in str(exc).lower():
raise
conn.execute(
"""
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL UNIQUE,
game TEXT NOT NULL,
user_game_id TEXT NOT NULL,
product TEXT NOT NULL,
amount INTEGER NOT NULL,
payment_method TEXT,
transaction_id TEXT,
screenshot TEXT,
status TEXT NOT NULL CHECK(status IN (
'pending',
'waiting_verification',
'paid',
'rejected',
'completed',
'expired'
)),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS transaction_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL,
action TEXT NOT NULL,
detail TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS order_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK(status IN (
'waiting',
'processing',
'completed',
'failed'
)),
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS smile_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
cookies TEXT,
balance INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL CHECK(status IN (
'active',
'paused',
'failed'
)),
last_used TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
except sqlite3.OperationalError as exc:
if "locked" not in str(exc).lower():
raise
print("Warning: orders.db is locked by another app. Close DB Browser if order creation fails.")
def row_to_dict(row):
if row is None:
return None
data = dict(row)
data["expires_at"] = created_ts(data["created_at"]) + ORDER_EXPIRY_SECONDS
data["seconds_remaining"] = max(0, data["expires_at"] - int(time.time()))
return data
def created_ts(created_at):
created = datetime.strptime(created_at, "%Y-%m-%d %H:%M:%S")
return int(created.replace(tzinfo=timezone.utc).timestamp())
def log_action(conn, order_id, action, detail=""):
conn.execute(
"INSERT INTO transaction_logs (order_id, action, detail) VALUES (?, ?, ?)",
(order_id, action, detail),
)
def enqueue_paid_order(conn, order_id):
conn.execute(
"""
INSERT INTO order_queue (order_id, status)
VALUES (?, 'waiting')
ON CONFLICT(order_id) DO UPDATE SET
status = CASE
WHEN order_queue.status IN ('completed', 'processing') THEN order_queue.status
ELSE 'waiting'
END,
updated_at = CURRENT_TIMESTAMP
""",
(order_id,),
)
log_action(conn, order_id, "queued", "Order is waiting for Smile.one automation")
def update_queue_status(order_id, status, detail=""):
if status not in {"waiting", "processing", "completed", "failed"}:
raise ValueError("Invalid queue status")
with db() as conn:
queue_row = conn.execute(
"SELECT * FROM order_queue WHERE order_id = ?",
(order_id,),
).fetchone()
if queue_row is None:
return None
if status == "processing":
conn.execute(
"""
UPDATE order_queue
SET status = ?, attempts = attempts + 1, last_error = NULL, updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?
""",
(status, order_id),
)
else:
conn.execute(
"""
UPDATE order_queue
SET status = ?, last_error = ?, updated_at = CURRENT_TIMESTAMP
WHERE order_id = ?
""",
(status, detail, order_id),
)
if status == "completed":
conn.execute("UPDATE orders SET status = 'completed' WHERE order_id = ?", (order_id,))
log_action(conn, order_id, "completed", detail or "automation completed")
elif status == "failed":
log_action(conn, order_id, "automation_failed", detail)
elif status == "processing":
log_action(conn, order_id, "automation_processing", detail)
elif status == "waiting":
log_action(conn, order_id, "automation_retry", detail)
return dict(
conn.execute("SELECT * FROM order_queue WHERE order_id = ?", (order_id,)).fetchone()
)
def expire_old_orders():
cutoff = int(time.time()) - ORDER_EXPIRY_SECONDS
with db() as conn:
rows = conn.execute(
"SELECT order_id FROM orders WHERE status = 'pending' AND strftime('%s', created_at) < ?",
(cutoff,),
).fetchall()
for row in rows:
conn.execute(
"UPDATE orders SET status = 'expired' WHERE order_id = ?",
(row["order_id"],),
)
log_action(conn, row["order_id"], "expired", "Payment upload window elapsed")
def get_order(order_id):
expire_old_orders()
with db() as conn:
return row_to_dict(
conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
)
def unique_order_id():
return f"ORD{int(time.time())}{random.randint(100, 999)}"
def final_amount(product):
base = None
for products in PRODUCT_MENU.values():
if product in products:
base = products[product]
break
if base is None:
raise ValueError("Unknown product")
return base + random.randint(11, 89)
def send_telegram(order):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return
caption = (
f"Payment waiting verification\n"
f"Order: {order['order_id']}\n"
f"Amount: {order['amount']} MMK\n"
f"Method: {order['payment_method']}\n"
f"Game: {order['game']}\n"
f"User ID: {order['user_game_id']}\n"
f"Product: {order['product']}\n"
f"Txn ID: {order.get('transaction_id') or '-'}\n\n"
"Verify inside KBZPay/AYA Pay before confirming."
)
keyboard = {
"inline_keyboard": [
[
{"text": "Confirm", "callback_data": f"confirm:{order['order_id']}"},
{"text": "Reject", "callback_data": f"reject:{order['order_id']}"},
],
[{"text": "Completed", "callback_data": f"complete:{order['order_id']}"}],
]
}
screenshot_path = BASE_DIR / order["screenshot"]
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendPhoto"
boundary = f"----codex{secrets.token_hex(8)}"
body = multipart_body(
boundary,
{
"chat_id": TELEGRAM_CHAT_ID,
"caption": caption,
"reply_markup": json.dumps(keyboard),
},
"photo",
screenshot_path,
)
request = urllib.request.Request(
url,
data=body,
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
)
try:
urllib.request.urlopen(request, timeout=10).read()
except Exception as exc:
with db() as conn:
log_action(conn, order["order_id"], "telegram_failed", str(exc))
def multipart_body(boundary, fields, file_field, file_path):
chunks = []
for name, value in fields.items():
chunks.append(f"--{boundary}\r\n".encode())
chunks.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
chunks.append(str(value).encode())
chunks.append(b"\r\n")
chunks.append(f"--{boundary}\r\n".encode())
chunks.append(
f'Content-Disposition: form-data; name="{file_field}"; filename="{file_path.name}"\r\n'.encode()
)
chunks.append(b"Content-Type: image/jpeg\r\n\r\n")
chunks.append(file_path.read_bytes())
chunks.append(b"\r\n")
chunks.append(f"--{boundary}--\r\n".encode())
return b"".join(chunks)
def telegram_answer_callback(callback_id, text):
if not TELEGRAM_BOT_TOKEN or not callback_id:
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/answerCallbackQuery"
data = urllib.parse.urlencode({"callback_query_id": callback_id, "text": text}).encode()
try:
urllib.request.urlopen(url, data=data, timeout=10).read()
except Exception:
pass
def send_telegram_text(text):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = urllib.parse.urlencode({"chat_id": TELEGRAM_CHAT_ID, "text": text}).encode()
try:
urllib.request.urlopen(url, data=data, timeout=10).read()
except Exception:
pass
def update_order_status(order_id, status, detail=""):
if status not in STATUSES:
raise ValueError("Invalid status")
with db() as conn:
order = conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
if order is None:
return None
conn.execute("UPDATE orders SET status = ? WHERE order_id = ?", (status, order_id))
if status == "paid":
enqueue_paid_order(conn, order_id)
log_action(conn, order_id, status, detail)
fresh = row_to_dict(
conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
)
if status == "paid":
send_telegram_text(f"{order_id} is paid and ready to process on Smile.one.")
elif status == "rejected":
send_telegram_text(f"{order_id} was rejected. Do not process this order.")
elif status == "completed":
send_telegram_text(f"{order_id} has been marked completed.")
return fresh
class Handler(BaseHTTPRequestHandler):
def do_HEAD(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/":
return self.head_file(PUBLIC_DIR / "index.html", "text/html")
if parsed.path.startswith("/uploads/"):
return self.head_file(UPLOAD_DIR / parsed.path.removeprefix("/uploads/"), guess_type(parsed.path))
if parsed.path.startswith("/"):
return self.head_file(PUBLIC_DIR / parsed.path.lstrip("/"), guess_type(parsed.path))
self.send_response(HTTPStatus.NOT_FOUND)
self.end_headers()
def do_GET(self):
expire_old_orders()
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/":
return self.serve_file(PUBLIC_DIR / "index.html", "text/html")
if parsed.path == "/api/config":
return self.json(
{
"account_name": ACCOUNT_NAME,
"kbzpay_number": KBZPAY_NUMBER,
"ayapay_number": AYAPAY_NUMBER,
"ayapay_qr_path": AYAPAY_QR_PATH,
"menu": PRODUCT_MENU,
"expiry_seconds": ORDER_EXPIRY_SECONDS,
}
)
if parsed.path.startswith("/api/orders/"):
order_id = parsed.path.rsplit("/", 1)[-1]
order = get_order(order_id)
if not order:
return self.error(HTTPStatus.NOT_FOUND, "Order not found")
return self.json({"order": order})
if parsed.path == "/api/admin/orders":
if not self.is_admin(parsed):
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token")
with db() as conn:
rows = conn.execute(
"SELECT * FROM orders ORDER BY id DESC LIMIT 100"
).fetchall()
return self.json({"orders": [row_to_dict(row) for row in rows]})
if parsed.path == "/api/admin/queue":
if not self.is_admin(parsed):
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token")
with db() as conn:
rows = conn.execute(
"""
SELECT q.*, o.game, o.user_game_id, o.product, o.amount
FROM order_queue q
JOIN orders o ON o.order_id = q.order_id
ORDER BY q.id ASC
LIMIT 100
"""
).fetchall()
return self.json({"queue": [dict(row) for row in rows]})
if parsed.path == "/api/admin/smile-accounts":
if not self.is_admin(parsed):
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token")
with db() as conn:
rows = conn.execute(
"""
SELECT id, email, balance, status, last_used, created_at, updated_at
FROM smile_accounts
ORDER BY status ASC, last_used ASC, id ASC
"""
).fetchall()
return self.json({"accounts": [dict(row) for row in rows]})
if parsed.path.startswith("/uploads/"):
return self.serve_file(UPLOAD_DIR / parsed.path.removeprefix("/uploads/"), guess_type(parsed.path))
if parsed.path.startswith("/"):
return self.serve_file(PUBLIC_DIR / parsed.path.lstrip("/"), guess_type(parsed.path))
return self.error(HTTPStatus.NOT_FOUND, "Not found")
def do_POST(self):
expire_old_orders()
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/api/orders":
return self.create_order()
if parsed.path.startswith("/api/orders/") and parsed.path.endswith("/payment"):
order_id = parsed.path.split("/")[3]
return self.upload_payment(order_id)
if parsed.path == "/api/telegram/webhook":
return self.telegram_webhook()
if parsed.path.startswith("/api/admin/orders/"):
if not self.is_admin(parsed):
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token")
parts = parsed.path.split("/")
if len(parts) == 6 and parts[5] in {"confirm", "reject", "complete"}:
status = {"confirm": "paid", "reject": "rejected", "complete": "completed"}[parts[5]]
order = update_order_status(parts[4], status, "admin panel action")
if not order:
return self.error(HTTPStatus.NOT_FOUND, "Order not found")
return self.json({"order": order})
if parsed.path.startswith("/api/admin/queue/"):
if not self.is_admin(parsed):
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid admin token")
parts = parsed.path.split("/")
if len(parts) == 6 and parts[5] in {"process", "complete", "fail", "retry"}:
payload = self.read_json()
detail = (payload.get("detail") or "").strip()
status = {
"process": "processing",
"complete": "completed",
"fail": "failed",
"retry": "waiting",
}[parts[5]]
queue_item = update_queue_status(parts[4], status, detail)
if not queue_item:
return self.error(HTTPStatus.NOT_FOUND, "Queue item not found")
return self.json({"queue_item": queue_item})
return self.error(HTTPStatus.NOT_FOUND, "Not found")
def create_order(self):
payload = self.read_json()
game = payload.get("game", "").strip()
user_game_id = payload.get("user_game_id", "").strip()
product = payload.get("product", "").strip()
if not game or not user_game_id or not product:
return self.error(HTTPStatus.BAD_REQUEST, "game, user_game_id, and product are required")
if game not in PRODUCT_MENU or product not in PRODUCT_MENU[game]:
return self.error(HTTPStatus.BAD_REQUEST, "Selected product is not available for this game")
try:
amount = final_amount(product)
except ValueError as exc:
return self.error(HTTPStatus.BAD_REQUEST, str(exc))
order_id = unique_order_id()
try:
with db() as conn:
conn.execute(
"""
INSERT INTO orders (order_id, game, user_game_id, product, amount, status)
VALUES (?, ?, ?, ?, ?, 'pending')
""",
(order_id, game, user_game_id, product, amount),
)
log_action(conn, order_id, "created", f"{product} for {user_game_id}")
except sqlite3.OperationalError as exc:
if "locked" in str(exc).lower():
return self.error(
HTTPStatus.SERVICE_UNAVAILABLE,
"Database is locked. Close DB Browser or any app currently editing orders.db, then try again.",
)
raise
return self.json({"order_id": order_id, "amount": amount})
def upload_payment(self, order_id):
order = get_order(order_id)
if not order:
return self.error(HTTPStatus.NOT_FOUND, "Order not found")
if order["status"] == "expired":
return self.error(HTTPStatus.GONE, "Order expired")
if order["status"] not in {"pending", "rejected"}:
return self.error(HTTPStatus.CONFLICT, "Payment upload is not available for this order")
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": self.headers.get("Content-Type"),
},
)
payment_method = (form.getfirst("payment_method") or "").strip()
transaction_id = (form.getfirst("transaction_id") or "").strip()
if payment_method not in {"KBZPay", "AYA Pay"}:
return self.error(HTTPStatus.BAD_REQUEST, "Invalid payment method")
file_item = form["screenshot"] if "screenshot" in form else None
if file_item is None or not file_item.filename:
return self.error(HTTPStatus.BAD_REQUEST, "Screenshot is required")
extension = Path(file_item.filename).suffix.lower()
if extension not in {".jpg", ".jpeg", ".png", ".webp"}:
return self.error(HTTPStatus.BAD_REQUEST, "Screenshot must be jpg, png, or webp")
safe_name = f"{order_id}-{secrets.token_hex(6)}{extension}"
output_path = UPLOAD_DIR / safe_name
with output_path.open("wb") as fh:
fh.write(file_item.file.read())
relative_path = f"uploads/{safe_name}"
with db() as conn:
conn.execute(
"""
UPDATE orders
SET screenshot = ?, payment_method = ?, transaction_id = ?, status = 'waiting_verification'
WHERE order_id = ?
""",
(relative_path, payment_method, transaction_id, order_id),
)
log_action(conn, order_id, "screenshot_uploaded", payment_method)
fresh = row_to_dict(
conn.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
)
send_telegram(fresh)
return self.json({"order": fresh})
def telegram_webhook(self):
payload = self.read_json()
callback = payload.get("callback_query") or {}
data = callback.get("data", "")
callback_id = callback.get("id")
if ":" not in data:
return self.json({"ok": True})
action, order_id = data.split(":", 1)
status_map = {"confirm": "paid", "reject": "rejected", "complete": "completed"}
if action not in status_map:
return self.json({"ok": True})
status = status_map[action]
detail = "telegram callback"
order = update_order_status(order_id, status, detail)
telegram_answer_callback(callback_id, f"{order_id} marked {status}" if order else "Order not found")
return self.json({"ok": bool(order), "order": order})
def read_json(self):
length = int(self.headers.get("Content-Length", "0"))
if length == 0:
return {}
return json.loads(self.rfile.read(length).decode())
def is_admin(self, parsed):
params = urllib.parse.parse_qs(parsed.query)
token = params.get("token", [""])[0]
return secrets.compare_digest(token, ADMIN_TOKEN)
def serve_file(self, path, content_type):
try:
resolved = path.resolve()
if not str(resolved).startswith(str(BASE_DIR)):
return self.error(HTTPStatus.FORBIDDEN, "Forbidden")
data = resolved.read_bytes()
except FileNotFoundError:
return self.error(HTTPStatus.NOT_FOUND, "Not found")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", content_type)
if resolved.parent == PUBLIC_DIR:
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def head_file(self, path, content_type):
try:
resolved = path.resolve()
if not str(resolved).startswith(str(BASE_DIR)):
self.send_response(HTTPStatus.FORBIDDEN)
self.end_headers()
return
size = resolved.stat().st_size
except FileNotFoundError:
self.send_response(HTTPStatus.NOT_FOUND)
self.end_headers()
return
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", content_type)
if resolved.parent == PUBLIC_DIR:
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(size))
self.end_headers()
def json(self, payload, status=HTTPStatus.OK):
data = json.dumps(payload).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def error(self, status, message):
return self.json({"error": message}, status)
def log_message(self, fmt, *args):
print(f"{self.address_string()} - {fmt % args}")
def guess_type(path):
if path.endswith(".html"):
return "text/html"
if path.endswith(".css"):
return "text/css"
if path.endswith(".js"):
return "application/javascript"
if path.endswith(".png"):
return "image/png"
if path.endswith(".jpg") or path.endswith(".jpeg"):
return "image/jpeg"
if path.endswith(".webp"):
return "image/webp"
if path.endswith(".svg"):
return "image/svg+xml"
return "application/octet-stream"
if __name__ == "__main__":
init_db()
port = int(os.getenv("PORT", "8000"))
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
print(f"Server running at http://127.0.0.1:{port}")
server.serve_forever()
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PID_FILE="$SCRIPT_DIR/server.pid"
LOG_FILE="$SCRIPT_DIR/server.log"
PORT="${PORT:-8000}"
ADMIN_TOKEN="${ADMIN_TOKEN:-test-token}"
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE")
if kill -0 "$OLD_PID" 2>/dev/null; then
echo "Server already running on port $PORT with PID $OLD_PID"
echo "Open: http://127.0.0.1:$PORT/"
exit 0
fi
rm -f "$PID_FILE"
fi
cd "$SCRIPT_DIR"
nohup env PORT="$PORT" ADMIN_TOKEN="$ADMIN_TOKEN" PYTHONUNBUFFERED=1 python3 server.py >> "$LOG_FILE" 2>&1 &
PID=$!
echo "$PID" > "$PID_FILE"
COUNT=0
READY=0
while kill -0 "$PID" 2>/dev/null; do
if curl -fsS "http://127.0.0.1:$PORT/api/config" >/dev/null 2>&1; then
READY=1
break
fi
COUNT=$((COUNT + 1))
if [ "$COUNT" -ge 20 ]; then
break
fi
sleep 0.25
done
if [ "$READY" -eq 1 ]; then
echo "Server started on port $PORT with PID $PID"
echo "Open: http://127.0.0.1:$PORT/"
echo "Log: $LOG_FILE"
else
rm -f "$PID_FILE"
echo "Server failed to start. Last log lines:"
tail -20 "$LOG_FILE" 2>/dev/null || true
exit 1
fi
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
PID_FILE="$SCRIPT_DIR/server.pid"
PORT="${PORT:-8000}"
PID=""
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
fi
if [ -z "$PID" ] || ! kill -0 "$PID" 2>/dev/null; then
PID=$(lsof -tiTCP:"$PORT" -sTCP:LISTEN 2>/dev/null || true)
fi
if [ -z "$PID" ]; then
rm -f "$PID_FILE"
echo "No server is running on port $PORT"
exit 0
fi
kill "$PID"
COUNT=0
while kill -0 "$PID" 2>/dev/null; do
COUNT=$((COUNT + 1))
if [ "$COUNT" -ge 20 ]; then
echo "Server did not stop after 10 seconds. Try: kill -9 $PID"
exit 1
fi
sleep 0.5
done
rm -f "$PID_FILE"
echo "Server stopped. PID was $PID"