Funkční generování QR kódů
This commit is contained in:
@@ -3,6 +3,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import axios from 'axios';
|
||||
import { formatDate } from './utils';
|
||||
|
||||
type PizzaSize = {
|
||||
varId: number,
|
||||
@@ -85,10 +86,7 @@ const downloadPizzy = async () => {
|
||||
export const fetchPizzy = async (): Promise<Pizza[]> => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const date_ob = new Date();
|
||||
const date = ("0" + date_ob.getDate()).slice(-2);
|
||||
const month = ("0" + (date_ob.getMonth() + 1)).slice(-2);
|
||||
const year = date_ob.getFullYear();
|
||||
const dateStr = year + "-" + month + "-" + date;
|
||||
const dateStr = formatDate(date_ob);
|
||||
const dataPath = path.join(tmpDir, `chefie-${dateStr}.json`);
|
||||
|
||||
if (fs.existsSync(dataPath)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { addPizzaOrder, createPizzaDay, deletePizzaDay, finishPizzaDelivery, fin
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fetchMenus } from "./restaurants";
|
||||
import { getQr } from "./qr";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV || 'production'
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
@@ -134,7 +135,7 @@ app.post("/api/finishDelivery", (req, res) => {
|
||||
if (!req.body?.login) {
|
||||
throw Error("Nebyl předán login");
|
||||
}
|
||||
const data = finishPizzaDelivery(req.body.login);
|
||||
const data = finishPizzaDelivery(req.body.login, req.body.bankAccount, req.body.bankAccountHolder);
|
||||
io.emit("message", data);
|
||||
res.status(200).json({});
|
||||
});
|
||||
@@ -148,6 +149,18 @@ app.post("/api/updateChoice", (req, res) => {
|
||||
res.status(200).json(data);
|
||||
});
|
||||
|
||||
app.get("/api/qr", (req, res) => {
|
||||
if (!req.query?.login || typeof req.query.login !== 'string') {
|
||||
throw Error("Nebyl předán login");
|
||||
}
|
||||
const img = getQr(req.query.login);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': img.length
|
||||
});
|
||||
res.end(img);
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`New client connected: ${socket.id}`);
|
||||
|
||||
|
||||
98
server/src/qr.ts
Normal file
98
server/src/qr.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import fs from "fs";
|
||||
import axios from "axios";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { formatDate } from "./utils";
|
||||
|
||||
const QR_GENERATOR_URL = 'https://api.paylibo.com/paylibo/generator/image';
|
||||
const COUNTRY_CODE = 'CZ';
|
||||
const CURRENCY_CODE = 'CZK';
|
||||
const QR_PIXEL_SIZE = 256;
|
||||
const tmpDir = os.tmpdir();
|
||||
|
||||
/**
|
||||
* Převede číslo účtu z BBAN do IBAN. Automaticky dopočítá kontrolní číslice.
|
||||
*
|
||||
* @param bankAccountNumber číslo účtu ve formátu BBAN (123456-0123456789/0100)
|
||||
*/
|
||||
function convertBbanToIban(bankAccountNumber: string): string {
|
||||
// TODO validovat číslo účtu stejně jako na klientovi, pro případ že sem někdo pošle nesmysl
|
||||
let prefix: string = '';
|
||||
let accountNumber: string = bankAccountNumber;
|
||||
if (bankAccountNumber.indexOf('-') >= 0) {
|
||||
const split = bankAccountNumber.split('-');
|
||||
prefix = split[0];
|
||||
accountNumber = split[1];
|
||||
}
|
||||
prefix = prefix.padStart(6, '0');
|
||||
const split = accountNumber.split('/');
|
||||
accountNumber = split[0].padStart(10, '0');
|
||||
const bankCode = split[1].padStart(4, '0');
|
||||
let iban = `${bankCode}${prefix}${accountNumber}${COUNTRY_CODE}00`;
|
||||
// Zatím napevno, nemá smysl řešit nic jiného než CZ
|
||||
iban = iban.replace('C', '12').replace('Z', '35');
|
||||
const remainder = BigInt(iban) % BigInt(97);
|
||||
const checkDigits = BigInt(98) - remainder;
|
||||
iban = `${COUNTRY_CODE}${checkDigits.toString()}${bankCode}${prefix}${accountNumber}`;
|
||||
if (iban.length !== 24) {
|
||||
throw Error("Neplatná délka sestaveného IBAN: " + iban.length + ", očekáváno 24");
|
||||
}
|
||||
return iban;
|
||||
}
|
||||
|
||||
function createNameHash(customerName: string): string {
|
||||
return crypto.createHash('md5').update(customerName).digest('hex');
|
||||
}
|
||||
|
||||
function createFilePath(nameHash: string): string {
|
||||
const fileName = `${formatDate(new Date())}_${nameHash}.png`;
|
||||
return path.join(tmpDir, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vygeneruje, uloží a vrátí unikátní ID obrázku platebního QR kódu s danými parametry.
|
||||
*
|
||||
* @param customerName jméno uživatele, pro kterého je QR kód generován
|
||||
* @param bankAccountNumber číslo cílového bankovního účtu ve formátu BBAN
|
||||
* @param bankAccountHolder jméno držitele cílového bankovního účtu
|
||||
* @param amount částka v Kč
|
||||
* @param message zpráva pro příjemce
|
||||
* @returns hash, pomocí kterého lze následně získat vygenerovaný obrázek
|
||||
*/
|
||||
export async function generateQr(customerName: string, bankAccountNumber: string, bankAccountHolder: string, amount: number, message: string): Promise<string> {
|
||||
// Zpráva pro příjemce nesmí dle standardu obsahovat '*' a být delší než 60 znaků
|
||||
if (message.indexOf('*') >= 0) {
|
||||
message = message.replace('*', '');
|
||||
}
|
||||
if (message.length > 60) {
|
||||
message = message.substring(0, 60);
|
||||
}
|
||||
const payload = {
|
||||
iban: convertBbanToIban(bankAccountNumber),
|
||||
amount,
|
||||
currency: CURRENCY_CODE,
|
||||
message,
|
||||
recipientName: bankAccountHolder,
|
||||
branding: false,
|
||||
compress: false,
|
||||
size: QR_PIXEL_SIZE,
|
||||
}
|
||||
const response = await axios.get(QR_GENERATOR_URL, { responseType: 'stream', params: { ...payload } });
|
||||
// Použijeme hash, abychom nemuseli řešit nepovolené znaky ve jménu uživatele
|
||||
const nameHash = createNameHash(customerName);
|
||||
const imgPath = createFilePath(nameHash);
|
||||
response.data.pipe(fs.createWriteStream(imgPath));
|
||||
return nameHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí obrázek s QR kódem, pokud existuje.
|
||||
*
|
||||
* @param customerName jméno uživatele
|
||||
* @returns data obrázku
|
||||
*/
|
||||
export function getQr(customerName: string): Buffer {
|
||||
const imgPath = createFilePath(createNameHash(customerName));
|
||||
return fs.readFileSync(imgPath);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import {ClientData, Locations, Order, Pizza, PizzaDayState, PizzaOrder, PizzaSize, UdalostEnum} from "./types";
|
||||
import {db} from "./database";
|
||||
import {formatDate, getHumanDate, getIsWeekend} from "./utils";
|
||||
import {callNotifikace} from "./notifikace";
|
||||
import { ClientData, Locations, Order, Pizza, PizzaDayState, PizzaOrder, PizzaSize, UdalostEnum } from "./types";
|
||||
import { db } from "./database";
|
||||
import { formatDate, getHumanDate, getIsWeekend } from "./utils";
|
||||
import { callNotifikace } from "./notifikace";
|
||||
import { generateQr } from "./qr";
|
||||
|
||||
/** Vrátí dnešní datum, případně fiktivní datum pro účely vývoje a testování. */
|
||||
function getToday(): Date {
|
||||
@@ -191,17 +192,18 @@ export function finishPizzaOrder(login: string) {
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.ORDERED;
|
||||
db.set(today, clientData);
|
||||
callNotifikace({input:{udalost:UdalostEnum.OBJEDNANA_PIZZA,user:clientData?.pizzaDay?.creator}})
|
||||
callNotifikace({ input: { udalost: UdalostEnum.OBJEDNANA_PIZZA, user: clientData?.pizzaDay?.creator } })
|
||||
return clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nastaví stav pizza day na "pizzy doručeny".
|
||||
* Vygeneruje QR kódy pro všechny objednatele, pokud objednávající má vyplněné číslo účtu a jméno.
|
||||
*
|
||||
* @param login login uživatele
|
||||
* @returns aktuální data pro uživatele
|
||||
*/
|
||||
export function finishPizzaDelivery(login: string) {
|
||||
export function finishPizzaDelivery(login: string, bankAccount?: string, bankAccountHolder?: string) {
|
||||
const today = formatDate(getToday());
|
||||
const clientData: ClientData = db.get(today);
|
||||
if (!clientData.pizzaDay) {
|
||||
@@ -214,6 +216,15 @@ export function finishPizzaDelivery(login: string) {
|
||||
throw Error("Pizza day není ve stavu " + PizzaDayState.ORDERED);
|
||||
}
|
||||
clientData.pizzaDay.state = PizzaDayState.DELIVERED;
|
||||
|
||||
// Vygenerujeme QR kód, pokud k tomu máme data
|
||||
if (bankAccount?.length && bankAccountHolder?.length) {
|
||||
for (const order of clientData.pizzaDay.orders) {
|
||||
let message = order.pizzaList.map(pizza => `Pizza ${pizza.name} (${pizza.size})`).join(', ');
|
||||
const price = order.pizzaList.map(pizza => pizza.price).reduce((partial, a) => partial + a, 0);
|
||||
generateQr(order.customer, bankAccount, bankAccountHolder, price, message).then(() => order.hasQr = true);
|
||||
}
|
||||
}
|
||||
db.set(today, clientData);
|
||||
return clientData;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface Order {
|
||||
customer: string, // jméno objednatele
|
||||
pizzaList: PizzaOrder[], // seznam objednaných pizz
|
||||
totalPrice: number, // celková cena všech objednaných pizz a krabic
|
||||
hasQr?: boolean, // true, pokud je k objednávce vygenerován QR kód pro platbu
|
||||
}
|
||||
|
||||
/** Stav pizza dne */
|
||||
|
||||
Reference in New Issue
Block a user