This commit is contained in:
seb
2026-07-06 01:31:11 +02:00
commit 917930d0fa
34 changed files with 2912 additions and 0 deletions

23
src/endpoints/category.js Normal file
View File

@@ -0,0 +1,23 @@
import { sendJson, serverTimestamp } from '../http.js';
import { getCategoryList } from '../queries/category-list.js';
export const method = 'GET';
export const path = '/v1/category';
export async function handle(_req, res, { url }) {
const cursor = Number(url.searchParams.get('lastChangedCategory')) || 0;
const limit = Number(url.searchParams.get('limit')) || 20;
const categories = await getCategoryList({ cursor, limit });
const timestamp = serverTimestamp();
return sendJson(
res,
200,
categories.map((category) => ({
...category,
updated_at: timestamp,
created_at: timestamp,
}))
);
}

6
src/endpoints/cimage.js Normal file
View File

@@ -0,0 +1,6 @@
import { createImageHandler } from './image-handler.js';
export const method = 'GET';
export const path = '/v1/cimage';
export const handle = createImageHandler();

71
src/endpoints/client.js Normal file
View File

@@ -0,0 +1,71 @@
import { sendJson, serverTimestamp } from '../http.js';
export const method = 'GET';
export const path = '/v1/client';
function buildClientStep1(config) {
const {
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
serverFingerprint,
} = config;
return {
authCode: null,
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
mandantName: null,
mandantDatabase: null,
serverFingerprint,
name: null,
serverTimestamp: serverTimestamp(),
};
}
function buildClientStep2(authCode, config) {
const {
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
mandantName,
mandantDatabase,
} = config;
return {
authCode,
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
mandantName,
mandantDatabase,
serverFingerprint: null,
name: null,
serverTimestamp: serverTimestamp(),
};
}
export function handle(req, res, { url, pairingStore, config }) {
const authCode = url.searchParams.get('authCode') || '';
const name = url.searchParams.get('name') || 'JTL-POS';
if (authCode.length <= 4 && authCode.length > 0) {
return sendJson(res, 200, buildClientStep1(config));
}
if (authCode.length === 6) {
if (pairingStore.hasPairingCode(authCode)) {
pairingStore.revokePairingCode(authCode);
pairingStore.registerDevice(config.authToken, name);
return sendJson(res, 200, buildClientStep2(authCode, config));
}
return sendJson(res, 400, { Message: 'Der Authentifizierungscode ist falsch.' });
}
return sendJson(res, 400, { Message: 'Keinen passenden Authentifizierungscode gefunden.' });
}

View File

@@ -0,0 +1,13 @@
import { sendJson } from '../http.js';
import { getCustomerGroupList } from '../queries/customer-groups.js';
export const method = 'GET';
export const path = '/v1/customergroup';
export async function handle(_req, res, { url }) {
const cursor = Number(url.searchParams.get('lastChangedCustomerGroup')) || 0;
const customerGroups = await getCustomerGroupList({ cursor });
return sendJson(res, 200, customerGroups);
}

View File

@@ -0,0 +1,21 @@
import { sendBinary, sendJson } from '../http.js';
import { resizeImage } from '../image-resize.js';
import { getImageByHash } from '../queries/image.js';
export function createImageHandler() {
return async function handle(_req, res, { url }) {
const path = url.searchParams.get('path');
if (!path) {
return sendJson(res, 400, { Message: "Missing required query parameter 'path'." });
}
const image = await getImageByHash(path, '200');
if (!image) {
return sendJson(res, 404, { Message: `No image was found for path '${path}'.` });
}
const buffer = await resizeImage(image.buffer);
return sendBinary(res, 200, buffer, image.contentType);
};
}

10
src/endpoints/index.js Normal file
View File

@@ -0,0 +1,10 @@
import * as category from './category.js';
import * as cimage from './cimage.js';
import * as client from './client.js';
import * as customergroup from './customergroup.js';
import * as init from './init.js';
import * as order from './order.js';
import * as pimage from './pimage.js';
import * as product from './product.js';
export const endpoints = [client, init, category, product, pimage, cimage, customergroup, order];

33
src/endpoints/init.js Normal file
View File

@@ -0,0 +1,33 @@
import { sendJson } from '../http.js';
import { getMaxExternalId } from '../order-log.js';
import { getCategoryCount } from '../queries/category-count.js';
import { getCustomerGroupCount } from '../queries/customer-groups.js';
import { getProductCount } from '../queries/product-count.js';
export const method = 'GET';
export const path = '/v1/init';
export async function handle(_req, res, { url }) {
const productCursor = Number(url.searchParams.get('lastChangedProduct')) || 0;
const categoryCursor = Number(url.searchParams.get('lastChangedCategory')) || 0;
const customerGroupCursor = Number(url.searchParams.get('lastChangedCustomerGroup')) || 0;
const [productCount, categoryCount, customerGroupCount] = await Promise.all([
getProductCount({ cursor: productCursor }),
getCategoryCount({ cursor: categoryCursor }),
getCustomerGroupCount({ cursor: customerGroupCursor }),
]);
return sendJson(res, 200, {
version: '1.10.12.0',
product_count: String(productCount),
category_count: String(categoryCount),
customer_count: '0',
customerGroup_count: String(customerGroupCount),
compositeProduct_count: '0',
configurationGroup_count: '0',
configurationItem_count: '0',
deletedEntity_count: '0',
max_orderId_count: getMaxExternalId(),
});
}

48
src/endpoints/order.js Normal file
View File

@@ -0,0 +1,48 @@
import { sendJson } from '../http.js';
import { logOrder } from '../order-log.js';
export const method = 'POST';
export const path = '/v1/order';
function parseBody(buffer) {
if (!buffer || !buffer.length) {
return null;
}
try {
return JSON.parse(buffer.toString('utf8'));
} catch {
return undefined;
}
}
function getOrders(body) {
if (!body || typeof body !== 'object') {
return [];
}
if (Array.isArray(body.orders)) {
return body.orders;
}
return [];
}
export async function handle(req, res) {
const body = parseBody(req.rawBody);
if (body === undefined) {
return sendJson(res, 500, []);
}
const orders = getOrders(body);
const results = orders.map((order) => {
logOrder(order);
const externalOrderId = String(order?.externalId ?? '');
return {
status: 'OK',
externalOrderId,
message: '',
};
});
return sendJson(res, 200, results);
}

6
src/endpoints/pimage.js Normal file
View File

@@ -0,0 +1,6 @@
import { createImageHandler } from './image-handler.js';
export const method = 'GET';
export const path = '/v1/pimage';
export const handle = createImageHandler();

67
src/endpoints/product.js Normal file
View File

@@ -0,0 +1,67 @@
import { sendJson } from '../http.js';
import { getProductList } from '../queries/product-list.js';
export const method = 'GET';
export const path = '/v1/product';
const STATIC_DEFAULTS = {
sort: '0',
p_price: '0.00',
discountable: '0',
deposit: '0',
discount: '',
d_price: '0.0',
tax_rate2: '',
use_in_out_tax: '0',
barcode: null,
use_stock: '0',
q_div: '0',
quantity: '0',
unit: null,
single_bookable: '0',
annotation: '',
status: '0',
tags: '',
is_parent: '0',
parent: '0',
variants: '',
print_kitchen_receipt: '0',
deposit_name: '',
updated_at: '0001-01-01 00:00:00',
isCompositeProduct: '0',
attributes: [],
configurationGroups: '',
options: null,
hasBestBeforeDate: '0',
hasLotNumber: '0',
hasSerialNumber: '0',
PLU: '',
short_description: '',
minStock: '0',
container: [],
reservedQuantity: '0.00',
deliveryDetails: [],
isbn: '',
manufacturerName: null,
han: null,
productType: '0',
voucherData: null,
inputPrice: '0',
inputQuantity: '0',
};
export async function handle(_req, res, { url }) {
const cursor = Number(url.searchParams.get('lastChangedProduct')) || 0;
const limit = Number(url.searchParams.get('limit')) || 20;
const products = await getProductList({ cursor, limit });
return sendJson(
res,
200,
products.map((product) => ({
...STATIC_DEFAULTS,
...product,
}))
);
}