1
This commit is contained in:
@@ -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
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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: "",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
);
|
||||
Generated
+936
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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="<b>Overall System Architecture</b>" 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="<b>Customer</b><br>Places order<br>Uploads payment proof<br>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="<b>Website</b><br>Frontend pages<br>Order form<br>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="<b>Backend API</b><br>Orders<br>Payments<br>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="<b>Database</b><br>orders<br>smile_accounts<br>logs<br>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="<b>Queue</b><br>waiting<br>processing<br>completed<br>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="<b>Puppeteer Worker</b><br>Reads queue<br>Runs automation<br>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="<b>Smile.one</b><br>Login/session<br>Game top-up<br>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="<b>Telegram Bot</b><br>Payment proof<br>Confirm / Reject<br>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="<b>Email System</b><br>Low balance<br>Failures<br>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="<b>Admin</b><br>Checks wallet<br>Controls orders<br>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="<b>Customer Order Flowchart</b>" 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<br><b>PUBG UC</b> or <b>MLBB Diamonds</b>" 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<br>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<br>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<br>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<br>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<br>Base price + random digits<br>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<br>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<br>Order ID<br>Exact amount<br>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="<b>Payment Verification Flowchart</b>" 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<br>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<br>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<br>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<br>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<br>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<br>Status: paid<br>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<br>Status: rejected<br>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="<b>Queue Processing Flowchart</b>" 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<br>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<br>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<br>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<br>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<br>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<br>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<br>Manual fallback<br>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="<b>Puppeteer Automation Flowchart</b>" 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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>Success / error text<br>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<br>Update order + queue<br>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<br>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<br>Retry or manual fallback<br>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="<b>Smile Account Pool Flowchart</b>" 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<br>Pause job<br>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<br>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<br>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<br>Update balance<br>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="<b>Balance Monitoring Flowchart</b>" 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<br>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<br>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<br>No alert<br>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<br>Warning: 50000 MMK<br>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="<b>Notification System Flowchart</b>" 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<br>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<br>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<br>Order/account details<br>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<br>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="<b>Error Handling Flowchart</b>" 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<br>Browser crash<br>CAPTCHA<br>Timeout<br>Invalid ID<br>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?<br>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?<br>Pause automation<br>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<br>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<br>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<br>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<br>Stack + screenshot<br>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="<b>Admin Dashboard Flowchart</b>" 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<br>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<br>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<br>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<br>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<br>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<br>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<br>Update database<br>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="<b>Game Top-Up Automation System</b><br><font style="font-size: 18px;">Development Roadmap for PUBG + MLBB reseller automation</font><br><font style="font-size: 13px;">Smile.one | Puppeteer | Telegram Bot | Email Notifications | KBZPay / AYA Pay Verification</font>" 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="<b>Goal:</b> 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="<b>PHASE 1 - Foundation Setup</b><br><font style="font-size: 12px;"><b>Goal:</b> Build the core backend and project structure.<br><b>Tasks:</b> Node.js + Express, server, API routes, env variables, SQLite first, MySQL later, orders/smile_accounts/logs tables, clean folders, GitHub repository.<br><b>Result:</b> Backend running, database connected, clean project structure.</font>" 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="<b>PHASE 2 - Customer Ordering System</b><br><font style="font-size: 12px;"><b>Goal:</b> Allow customers to place orders.<br><b>Tasks:</b> 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.<br><b>Result:</b> Customers can place orders and receive payment instructions.</font>" 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="<b>PHASE 3 - Payment Verification System</b><br><font style="font-size: 12px;"><b>Goal:</b> Accept KBZPay / AYA Pay screenshot payments.<br><b>Tasks:</b> 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.<br><b>Result:</b> Working payment system with Telegram admin control.</font>" 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="<b>PHASE 4 - Browser Automation</b><br><font style="font-size: 12px;"><b>Goal:</b> Automate Smile.one purchases. This is the most important stability phase.<br><b>Tasks:</b> 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.<br><b>Result:</b> System can automatically purchase products.</font>" 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="<b>PHASE 5 - Queue System</b><br><font style="font-size: 12px;"><b>Goal:</b> Process orders safely and sequentially.<br><b>Tasks:</b> Build order queue, connect New Order -> Queue -> Worker -> Automation, run worker continuously, process one-by-one, retry once after automation failure, fallback to manual processing.<br><b>Result:</b> Stable automation flow.</font>" 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="<b>PHASE 6 - Smile Account Pool System</b><br><font style="font-size: 12px;"><b>Goal:</b> Support multiple Smile.one accounts.<br><b>Tasks:</b> 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 -> Account A and Order 2 -> Account B.<br><b>Result:</b> Scalable multi-account automation.</font>" 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="<b>PHASE 7 - Balance Monitoring System</b><br><font style="font-size: 12px;"><b>Goal:</b> Track Smile balances automatically.<br><b>Tasks:</b> 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.<br><b>Result:</b> Real-time balance monitoring.</font>" 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="<b>PHASE 8 - Email Notification System</b><br><font style="font-size: 12px;"><b>Goal:</b> Create professional system alerts.<br><b>Tasks:</b> Setup Nodemailer with Gmail SMTP, add alert types for low balance, automation failure, and critical errors, add cooldown logic to prevent email spam.<br><b>Result:</b> Reliable notification system.</font>" 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="<b>PHASE 9 - Security and Stability</b><br><font style="font-size: 12px;"><b>Goal:</b> Make the system production-ready.<br><b>Tasks:</b> Add HTTPS, protect admin routes, add rate limiting, store automation logs, payment logs, and errors, add backup recovery and manual processing fallback.<br><b>Result:</b> Stable and secure system.</font>" 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="<b>PHASE 10 - UI and Dashboard</b><br><font style="font-size: 12px;"><b>Goal:</b> Improve user experience.<br><b>Tasks:</b> Build admin dashboard with orders, balances, revenue, failed orders; build customer dashboard with order tracking and history; improve mobile responsive design.<br><b>Result:</b> Professional reseller platform.</font>" 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="<b>PHASE 11 - Scaling and Optimization</b><br><font style="font-size: 12px;"><b>Goal:</b> Prepare for larger traffic.<br><b>Tasks:</b> Move to Ubuntu VPS, use Docker, run headless automation, optimize workers, add analytics for sales, profits, and top products.<br><b>Result:</b> System is ready for higher traffic and operational scaling.</font>" 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="<b>FINAL SYSTEM</b><br><font style="font-size: 13px;">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</font>" 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="<b>Final System Flow</b><br><font style="font-size: 14px;">Customer order to payment verification to queue-driven Smile.one automation</font>" 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="<b>Customer</b><br>Selects PUBG / MLBB<br>Enters game ID<br>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="<b>Frontend</b><br>Homepage<br>Product pages<br>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="<b>Backend API</b><br>Orders<br>Payments<br>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="<b>Database</b><br>orders<br>smile_accounts<br>logs<br>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="<b>Order Queue</b><br>waiting<br>processing<br>completed<br>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="<b>Worker</b><br>Processes one-by-one<br>Retries once<br>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="<b>Puppeteer</b><br>Saved sessions<br>Visible test mode<br>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="<b>Smile.one</b><br>Login session<br>Game page<br>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="<b>Smile Account Pool</b><br>email<br>cookies/session<br>balance<br>status<br>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="<b>Payments</b><br>KBZPay<br>AYA Pay<br>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="<b>Telegram Bot</b><br>Order details<br>Screenshot<br>Confirm / Reject<br>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="<b>Live Checking</b><br>Health checks<br>Balance checks<br>Queue errors<br>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="<b>Email Notifications</b><br>Low balance<br>Automation failure<br>Critical errors<br>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="<b>Admin Dashboard</b><br>Orders<br>Balances<br>Revenue<br>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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
Generated
+37
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
Executable
+7
@@ -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"
|
||||
@@ -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 -
|
||||
@@ -0,0 +1 @@
|
||||
70547
|
||||
@@ -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()
|
||||
Executable
+49
@@ -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
|
||||
Executable
+36
@@ -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"
|
||||
Reference in New Issue
Block a user