Rewrite store JSDoc, split quote/order into modules, and generate typed API docs
- Rewrite JSDoc across all store methods to document the actual async methods (real params, returns, throws) instead of the internal wrappers - Split quote/order into individual module files backed by shared manifestDoc helpers; remove the createStore factory and thin wrappers - Deduplicate manifest line helpers into src/store/manifestLines.js - Extend generate-api-doc.js to render parameter/return/throws types and emit per-namespace API.<module>.md files with an API.md index - Regenerate all API docs
This commit is contained in:
@@ -5,20 +5,21 @@ import newFullHash from '../../util/newFullHash.js';
|
||||
import request from '../request.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Adds an image to an article by its SKU, processing it into AVIF variants.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that adds an article image.
|
||||
*
|
||||
* @example
|
||||
* const fn = addImage(pool);
|
||||
* const result = await fn(42, 'SKU-1', imageBuffer);
|
||||
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
||||
*/
|
||||
export default function addImage(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
|
||||
/**
|
||||
* Adds an image to an article by its SKU, processing it into AVIF variants.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {Buffer} image - The source image buffer.
|
||||
* @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.article.addImage(42, 'SKU-1', imageBuffer);
|
||||
* // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash }
|
||||
*/
|
||||
return async (googleAuthId, sku, image) => {
|
||||
const { articleId } = await resolve(googleAuthId, sku);
|
||||
|
||||
|
||||
@@ -7,23 +7,25 @@ import nextSku from '../numbers/nextSku.js';
|
||||
import request from '../request.js';
|
||||
import slugify from '../slugify.js';
|
||||
|
||||
/**
|
||||
* Creates a new article under a given parent category, allocating a SKU and
|
||||
* a default unit.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates an article.
|
||||
*
|
||||
* @example
|
||||
* const fn = createUnderParent(pool);
|
||||
* const article = await fn({ googleAuthId: 42, parentId: 5, name: 'Cola' });
|
||||
* // => { id, parentId, name, sku }
|
||||
*/
|
||||
export default function createUnderParent(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
const ensureUnit = ensureDefaultUnit(pool);
|
||||
const allocateSku = nextSku(pool);
|
||||
|
||||
/**
|
||||
* Creates a new article under a given parent category, allocating a SKU and
|
||||
* a default unit.
|
||||
*
|
||||
* @param {object} input - The article creation input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.parentId - The id of the parent category.
|
||||
* @param {string} input.name - The article name.
|
||||
* @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' });
|
||||
* // => { id, parentId, name, sku }
|
||||
*/
|
||||
return async ({ googleAuthId, parentId, name }) => {
|
||||
const { companyId, languageId } = await access(googleAuthId, parentId);
|
||||
|
||||
|
||||
@@ -11,19 +11,20 @@ import {
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import request from '../request.js';
|
||||
|
||||
/**
|
||||
* Deletes an article by its SKU, refusing when it still has children.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes an article.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 'SKU-1');
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
|
||||
/**
|
||||
* Deletes an article by its SKU, refusing when it still has children.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the article has children or is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.article.delete(42, 'SKU-1');
|
||||
*/
|
||||
return async (googleAuthId, sku) => {
|
||||
const { articleId, languageId } = await resolve(googleAuthId, sku);
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { DELETE_BY_ARTICLE_AND_HASH } from '../../sql/pic.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Deletes an image from an article by its SKU and full image hash.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes an article image.
|
||||
*
|
||||
* @example
|
||||
* const fn = deleteImage(pool);
|
||||
* await fn(42, 'SKU-1', 'abc123');
|
||||
*/
|
||||
export default function deleteImage(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
|
||||
/**
|
||||
* Deletes an image from an article by its SKU and full image hash.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {string} fullHash - The full image hash to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the image is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.article.deleteImage(42, 'SKU-1', 'abc123');
|
||||
*/
|
||||
return async (googleAuthId, sku, fullHash) => {
|
||||
const { articleId } = await resolve(googleAuthId, sku);
|
||||
|
||||
|
||||
@@ -6,19 +6,20 @@ function parseJsonArray(raw) {
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full detail of an article by its SKU, including unit, brand,
|
||||
* categories, photos, EANs and prices.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches an article by SKU.
|
||||
*
|
||||
* @example
|
||||
* const fn = getBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1');
|
||||
* // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } }
|
||||
*/
|
||||
export default function getBySku(pool) {
|
||||
/**
|
||||
* Returns the full detail of an article by its SKU, including unit, brand,
|
||||
* categories, photos, EANs and prices.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @returns {Promise<{article: object}>} The full article detail.
|
||||
* @throws {Error} If the article is not found or the user is not authorized.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.getBySku(42, 'SKU-1');
|
||||
* // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } }
|
||||
*/
|
||||
return async (googleAuthId, sku) => {
|
||||
const t0 = performance.now();
|
||||
const result = await pool
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { LIST_BY_ARTICLE_ID } from '../../sql/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Returns the change history of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches the article history.
|
||||
*
|
||||
* @example
|
||||
* const fn = getHistoryBySku(pool);
|
||||
* const history = await fn(42, 'SKU-1');
|
||||
* // => [{ id, companyuserId, date, text }]
|
||||
*/
|
||||
export default function getHistoryBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
|
||||
/**
|
||||
* Returns the change history of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
||||
*
|
||||
* @example
|
||||
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||
* // => [{ id, companyuserId, date, text }]
|
||||
*/
|
||||
return async (googleAuthId, sku) => {
|
||||
const { articleId } = await resolve(googleAuthId, sku);
|
||||
|
||||
|
||||
@@ -10,22 +10,23 @@ function parseJsonArray(raw) {
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns articles for export, optionally scoped to a category, including
|
||||
* category breadcrumbs and image hashes.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists articles for export.
|
||||
*
|
||||
* @example
|
||||
* const fn = listExport(pool);
|
||||
* const articles = await fn(42, null);
|
||||
* // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
||||
*/
|
||||
export default function listExport(pool) {
|
||||
const companyAccess = resolveSelectedCompanyAccess(pool);
|
||||
const categoryAccess = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns articles for export, optionally scoped to a category, including
|
||||
* category breadcrumbs and image hashes.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number|null} [categoryId] - Optional category id to scope the export to; defaults to the company root.
|
||||
* @returns {Promise<Array<object>>} The articles for export.
|
||||
* @throws {Error} If the category is not reachable from the selected company.
|
||||
*
|
||||
* @example
|
||||
* const articles = await store.article.listExport(42, null);
|
||||
* // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
|
||||
*/
|
||||
return async (googleAuthId, categoryId) => {
|
||||
const { languageId, posCustomerGroupId, rootCategoryId } =
|
||||
await companyAccess(googleAuthId);
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { SEARCH_ARTICLES } from '../../sql/article.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Returns a lightweight search index of all articles for a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists the article search index.
|
||||
*
|
||||
* @example
|
||||
* const fn = listSearchIndex(pool);
|
||||
* const articles = await fn(42);
|
||||
* // => [{ id, sku, name, eans, categoryIds }]
|
||||
*/
|
||||
export default function listSearchIndex(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a lightweight search index of all articles for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>} The article search index.
|
||||
*
|
||||
* @example
|
||||
* const articles = await store.article.listSearchIndex(42);
|
||||
* // => [{ id, sku, name, eans, categoryIds }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId, rootCategoryId, languageId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -6,20 +6,23 @@ import {
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import request from '../request.js';
|
||||
|
||||
/**
|
||||
* Reorders the images of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that reorders article images.
|
||||
*
|
||||
* @example
|
||||
* const fn = orderImages(pool);
|
||||
* const result = await fn(42, 'SKU-1', { fullHashes: ['a', 'b'] });
|
||||
* // => { fullHashes: ['a', 'b'] }
|
||||
*/
|
||||
export default function orderImages(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
|
||||
/**
|
||||
* Reorders the images of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {object} input - The reorder input.
|
||||
* @param {Array<string>} input.fullHashes - The full image hashes in their new order.
|
||||
* @returns {Promise<{fullHashes: Array<string>}>} The applied order.
|
||||
* @throws {Error} If the order is invalid.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] });
|
||||
* // => { fullHashes: ['a', 'b'] }
|
||||
*/
|
||||
return async (googleAuthId, sku, { fullHashes }) => {
|
||||
if (!Array.isArray(fullHashes)) {
|
||||
throw new Error('Invalid image order');
|
||||
|
||||
@@ -5,21 +5,23 @@ import {
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Sets (or clears) the brand of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets the article brand.
|
||||
*
|
||||
* @example
|
||||
* const fn = setBrandIdBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', 3);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function setBrandIdBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Sets (or clears) the brand of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {number|null} brandId - The brand id, or `null` to clear the brand.
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If the brand or article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, brandId) => {
|
||||
const { articleId, languageId, companyId } = await resolve(
|
||||
googleAuthId,
|
||||
|
||||
@@ -7,22 +7,24 @@ import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Replaces the category membership of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets the article categories.
|
||||
*
|
||||
* @example
|
||||
* const fn = setCategoryIdsBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', [5, 9]);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function setCategoryIdsBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const categoryAccess = resolveCategoryAccess(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Replaces the category membership of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {Array<number>} categoryIds - The category ids the article should belong to (at least one).
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If the category ids are invalid or the article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, categoryIds) => {
|
||||
if (!Array.isArray(categoryIds)) {
|
||||
throw new Error('Invalid categoryIds');
|
||||
|
||||
@@ -6,21 +6,23 @@ import request from '../request.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Replaces the EAN list of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets article EANs.
|
||||
*
|
||||
* @example
|
||||
* const fn = setEansBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', ['4001234567890']);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function setEansBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Replaces the EAN list of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {Array<string>} eans - The EANs to set (must be unique, non-empty strings).
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If the EANs are invalid or the article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, eans) => {
|
||||
if (!Array.isArray(eans)) {
|
||||
throw new Error('Invalid eans');
|
||||
|
||||
@@ -42,22 +42,24 @@ function normalizePrices(prices) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the price list of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets article prices.
|
||||
*
|
||||
* @example
|
||||
* const fn = setPricesBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function setPricesBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Replaces the price list of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {Array<{price: number, tax: number, minmultiplier?: number}>} prices - The price entries (minmultiplier defaults to 1).
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If the prices are invalid or the article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, prices) => {
|
||||
const normalized = normalizePrices(prices);
|
||||
|
||||
|
||||
@@ -2,21 +2,23 @@ import { UPDATE_WEIGHT } from '../../sql/article.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
|
||||
/**
|
||||
* Sets the weight of an article by its SKU.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets the article weight.
|
||||
*
|
||||
* @example
|
||||
* const fn = setWeightBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', 1.5);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function setWeightBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Sets the weight of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {number} weight - The weight (must be >= 0).
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If the weight is invalid or the article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5);
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, weight) => {
|
||||
if (!Number.isFinite(weight) || weight < 0) {
|
||||
throw new Error('Invalid weight');
|
||||
|
||||
@@ -20,21 +20,26 @@ function formatUnitLabel(amount, symbol) {
|
||||
return `${Number(amount)}${sym}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an article's name and/or unit (SI and amount), recording history.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates an article by SKU.
|
||||
*
|
||||
* @example
|
||||
* const fn = updateBySku(pool);
|
||||
* const article = await fn(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 });
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
export default function updateBySku(pool) {
|
||||
const resolve = resolveArticleBySku(pool);
|
||||
const load = queryBySku(pool);
|
||||
|
||||
/**
|
||||
* Updates an article's name and/or unit (SI and amount), recording history.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {object} patch - The update patch.
|
||||
* @param {string} [patch.name] - The new article name.
|
||||
* @param {string} [patch.unitSi] - The new unit SI code.
|
||||
* @param {number} [patch.unitamount] - The new unit amount (must be > 0).
|
||||
* @returns {Promise<object>} The updated article.
|
||||
* @throws {Error} If no valid field is provided or the article is not found.
|
||||
*
|
||||
* @example
|
||||
* const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 });
|
||||
* // => { id, sku, name, ... }
|
||||
*/
|
||||
return async (googleAuthId, sku, patch = {}) => {
|
||||
const hasName = Object.prototype.hasOwnProperty.call(patch, 'name');
|
||||
const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount');
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_COMPANY_AND_ID } from '../../sql/brand.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a brand for a company by its id.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a brand.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 3);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a brand for the user's selected company by its id.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the brand to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the brand is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.brand.delete(42, 3);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Brand not found');
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { LIST_BY_COMPANY } from '../../sql/brand.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Lists the brands of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists brands.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const brands = await fn(42);
|
||||
* // => [{ id, name, gsprpinfo }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the brands of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, name: string, gsprpinfo: string|null}>>} The brands.
|
||||
*
|
||||
* @example
|
||||
* const brands = await store.brand.list(42);
|
||||
* // => [{ id, name, gsprpinfo }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -5,20 +5,23 @@ import {
|
||||
} from '../../sql/brand.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Creates or updates a brand for a company, keyed by name.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that upserts a brand.
|
||||
*
|
||||
* @example
|
||||
* const fn = upsert(pool);
|
||||
* const brand = await fn(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
|
||||
* // => { id, name, gsprpinfo }
|
||||
*/
|
||||
export default function upsert(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates or updates a brand for the user's selected company, keyed by name.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The brand input.
|
||||
* @param {string} input.name - The brand name.
|
||||
* @param {string} [input.gsprpinfo] - Optional GSPRP info.
|
||||
* @returns {Promise<{id: number, name: string, gsprpinfo: string|null}>} The upserted brand.
|
||||
* @throws {Error} If the brand is invalid.
|
||||
*
|
||||
* @example
|
||||
* const brand = await store.brand.upsert(42, { name: 'Coca-Cola', gsprpinfo: 'x' });
|
||||
* // => { id, name, gsprpinfo }
|
||||
*/
|
||||
return async (googleAuthId, input = {}) => {
|
||||
const name = input.name?.trim();
|
||||
if (!name) {
|
||||
|
||||
@@ -5,20 +5,21 @@ import newFullHash from '../../util/newFullHash.js';
|
||||
import request from '../request.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
|
||||
/**
|
||||
* Adds an image to a category, processing it into AVIF full and thumb variants.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that adds a category image.
|
||||
*
|
||||
* @example
|
||||
* const fn = addImage(pool);
|
||||
* const result = await fn(42, 5, imageBuffer);
|
||||
* // => { id, fullHash, thumbW, thumbH, thumbHash }
|
||||
*/
|
||||
export default function addImage(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Adds an image to a category, processing it into AVIF full and thumb variants.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} categoryId - The id of the category.
|
||||
* @param {Buffer} image - The source image buffer.
|
||||
* @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.category.addImage(42, 5, imageBuffer);
|
||||
* // => { id, fullHash, thumbW, thumbH, thumbHash }
|
||||
*/
|
||||
return async (googleAuthId, categoryId, image) => {
|
||||
await access(googleAuthId, categoryId);
|
||||
|
||||
|
||||
@@ -7,20 +7,22 @@ import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
import request from '../request.js';
|
||||
import slugify from '../slugify.js';
|
||||
|
||||
/**
|
||||
* Creates a new category under a given parent category.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates a category.
|
||||
*
|
||||
* @example
|
||||
* const fn = createUnderParent(pool);
|
||||
* const category = await fn({ googleAuthId: 42, parentId: 5, name: 'Drinks' });
|
||||
* // => { id, parentId, sort, name, slug }
|
||||
*/
|
||||
export default function createUnderParent(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates a new category under a given parent category.
|
||||
*
|
||||
* @param {object} input - The category creation input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.parentId - The id of the parent category.
|
||||
* @param {string} input.name - The category name.
|
||||
* @returns {Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>} The created category.
|
||||
*
|
||||
* @example
|
||||
* const category = await store.category.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Drinks' });
|
||||
* // => { id, parentId, sort, name, slug }
|
||||
*/
|
||||
return async ({ googleAuthId, parentId, name }) => {
|
||||
const { languageId } = await access(googleAuthId, parentId);
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { DELETE } from '../../sql/category.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a category, refusing to delete the company's root category.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a category.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn({ googleAuthId: 42, id: 5 });
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a category, refusing to delete the company's root category.
|
||||
*
|
||||
* @param {object} input - The delete input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.id - The id of the category to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the category is the root category.
|
||||
*
|
||||
* @example
|
||||
* await store.category.delete({ googleAuthId: 42, id: 5 });
|
||||
*/
|
||||
return async ({ googleAuthId, id }) => {
|
||||
const { rootCategoryId } = await access(googleAuthId, id);
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { DELETE_BY_CATEGORY } from '../../sql/categorypic.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes the image of a category.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a category image.
|
||||
*
|
||||
* @example
|
||||
* const fn = deleteImage(pool);
|
||||
* const result = await fn(42, 5);
|
||||
* // => { id, thumbW: null, thumbH: null, thumbHash: null }
|
||||
*/
|
||||
export default function deleteImage(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes the image of a category.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} categoryId - The id of the category.
|
||||
* @returns {Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>} The cleared image info.
|
||||
* @throws {Error} If the image is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.category.deleteImage(42, 5);
|
||||
* // => { id: 5, thumbW: null, thumbH: null, thumbHash: null }
|
||||
*/
|
||||
return async (googleAuthId, categoryId) => {
|
||||
await access(googleAuthId, categoryId);
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
import queryCategoryArticles from './queryCategoryArticles.js';
|
||||
|
||||
/**
|
||||
* Returns the articles belonging to a single category.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches the articles of a category.
|
||||
*
|
||||
* @example
|
||||
* const fn = getCategoryArticles(pool);
|
||||
* const articles = await fn(42, 5);
|
||||
* // => [{ id, sku, name, ... }]
|
||||
*/
|
||||
export default function getCategoryArticles(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
const articles = queryCategoryArticles(pool);
|
||||
|
||||
/**
|
||||
* Returns the articles belonging to a single category.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} categoryId - The id of the category.
|
||||
* @returns {Promise<Array<object>>} The articles in the category.
|
||||
*
|
||||
* @example
|
||||
* const articles = await store.category.getCategoryArticles(42, 5);
|
||||
* // => [{ id, sku, name, ... }]
|
||||
*/
|
||||
return async (googleAuthId, categoryId) => {
|
||||
const { languageId, posCustomerGroupId } = await access(
|
||||
googleAuthId,
|
||||
|
||||
@@ -3,23 +3,22 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
|
||||
import queryCategoryArticles from './queryCategoryArticles.js';
|
||||
import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js';
|
||||
|
||||
/**
|
||||
* Returns the full category tree for a company together with its articles and
|
||||
* the posterminal ids assigned to each category.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches the category tree.
|
||||
*
|
||||
* @example
|
||||
* const fn = getTreeForCompany(pool);
|
||||
* const tree = await fn(42);
|
||||
* // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
||||
*/
|
||||
export default function getTreeForCompany(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const articles = queryCategoryArticles(pool);
|
||||
const posterminalIds = getCategoryPosterminalIds(pool);
|
||||
|
||||
/**
|
||||
* Returns the full category tree for the user's selected company together
|
||||
* with its articles and the posterminal ids assigned to each category.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<{languageId: number, tree: Array<object>, articles: Array<object>}>} The category tree, articles and language.
|
||||
*
|
||||
* @example
|
||||
* const tree = await store.category.getTreeForCompany(42);
|
||||
* // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] }
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId, rootCategoryId, languageId, posCustomerGroupId } =
|
||||
await access(googleAuthId);
|
||||
|
||||
@@ -6,20 +6,23 @@ import {
|
||||
import request from '../request.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
|
||||
/**
|
||||
* Moves a category to a new parent, preventing cycles and cross-company moves.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that moves a category.
|
||||
*
|
||||
* @example
|
||||
* const fn = move(pool);
|
||||
* const result = await fn({ googleAuthId: 42, id: 5, parentId: 9 });
|
||||
* // => { id, parentId, sort }
|
||||
*/
|
||||
export default function move(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Moves a category to a new parent, preventing cycles and cross-company moves.
|
||||
*
|
||||
* @param {object} input - The move input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.id - The id of the category to move.
|
||||
* @param {number} input.parentId - The id of the new parent category.
|
||||
* @returns {Promise<{id: number, parentId: number, sort: number}>} The moved category.
|
||||
* @throws {Error} If the move would create a cycle or cross companies.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.category.move({ googleAuthId: 42, id: 5, parentId: 9 });
|
||||
* // => { id, parentId, sort }
|
||||
*/
|
||||
return async ({ googleAuthId, id, parentId }) => {
|
||||
// Source category auth (throws if missing / not authorized).
|
||||
const source = await access(googleAuthId, id);
|
||||
|
||||
@@ -5,20 +5,23 @@ import {
|
||||
import request from '../request.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
|
||||
/**
|
||||
* Reorders the child categories of a given parent.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that reorders categories.
|
||||
*
|
||||
* @example
|
||||
* const fn = order(pool);
|
||||
* const result = await fn({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] });
|
||||
* // => { categoryIds: [3, 1, 2] }
|
||||
*/
|
||||
export default function order(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Reorders the child categories of a given parent.
|
||||
*
|
||||
* @param {object} input - The reorder input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.parentId - The id of the parent category.
|
||||
* @param {Array<number>} input.categoryIds - The child category ids in their new order.
|
||||
* @returns {Promise<{categoryIds: Array<number>}>} The applied order.
|
||||
* @throws {Error} If the order is invalid.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.category.order({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] });
|
||||
* // => { categoryIds: [3, 1, 2] }
|
||||
*/
|
||||
return async ({ googleAuthId, parentId, categoryIds }) => {
|
||||
if (!Array.isArray(categoryIds)) {
|
||||
throw new Error('Invalid category order');
|
||||
|
||||
@@ -2,20 +2,23 @@ import { UPDATE_BY_CATEGORY_AND_LANGUAGE } from '../../sql/categorytext.js';
|
||||
import resolveCategoryAccess from './resolveCategoryAccess.js';
|
||||
import slugify from '../slugify.js';
|
||||
|
||||
/**
|
||||
* Renames a category and updates its slug.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that renames a category.
|
||||
*
|
||||
* @example
|
||||
* const fn = rename(pool);
|
||||
* const result = await fn({ googleAuthId: 42, id: 5, name: 'Soft Drinks' });
|
||||
* // => { id, name, slug }
|
||||
*/
|
||||
export default function rename(pool) {
|
||||
const access = resolveCategoryAccess(pool);
|
||||
|
||||
/**
|
||||
* Renames a category and updates its slug.
|
||||
*
|
||||
* @param {object} input - The rename input.
|
||||
* @param {number} input.googleAuthId - The id of the Google auth user.
|
||||
* @param {number} input.id - The id of the category to rename.
|
||||
* @param {string} input.name - The new category name.
|
||||
* @returns {Promise<{id: number, name: string, slug: string}>} The renamed category.
|
||||
* @throws {Error} If the name is empty or the category is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.category.rename({ googleAuthId: 42, id: 5, name: 'Soft Drinks' });
|
||||
* // => { id, name, slug }
|
||||
*/
|
||||
return async ({ googleAuthId, id, name }) => {
|
||||
if (!name?.trim()) {
|
||||
throw new Error('Name required');
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a company setting by key.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a company setting.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 'theme');
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a company setting by key for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} key - The setting key.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the setting is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.companysetting.delete(42, 'theme');
|
||||
*/
|
||||
return async (googleAuthId, key) => {
|
||||
if (!key?.trim()) {
|
||||
throw new Error('Setting not found');
|
||||
|
||||
@@ -2,20 +2,22 @@ import { GET_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import shapeSetting from './shapeSetting.js';
|
||||
|
||||
/**
|
||||
* Returns a single company setting by key.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a company setting.
|
||||
*
|
||||
* @example
|
||||
* const fn = get(pool);
|
||||
* const setting = await fn(42, 'theme');
|
||||
* // => { key, value, frontend }
|
||||
*/
|
||||
export default function get(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a single company setting by key for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} key - The setting key.
|
||||
* @param {object} [options] - Optional options.
|
||||
* @param {boolean} [options.frontendOnly] - Only return the setting if it is frontend-visible.
|
||||
* @returns {Promise<{key: string, value: string, frontend: boolean}|undefined>} The setting, or `undefined` if not found.
|
||||
*
|
||||
* @example
|
||||
* const setting = await store.companysetting.get(42, 'theme');
|
||||
* // => { key, value, frontend }
|
||||
*/
|
||||
return async (googleAuthId, key, { frontendOnly = false } = {}) => {
|
||||
if (!key?.trim()) {
|
||||
return undefined;
|
||||
|
||||
@@ -5,20 +5,21 @@ import {
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import shapeSetting from './shapeSetting.js';
|
||||
|
||||
/**
|
||||
* Lists the settings of a company, optionally only frontend-visible ones.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists company settings.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const settings = await fn(42, { frontendOnly: true });
|
||||
* // => [{ key, value, frontend }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the settings of the user's selected company, optionally only frontend-visible ones.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [options] - Optional options.
|
||||
* @param {boolean} [options.frontendOnly] - Only return frontend-visible settings.
|
||||
* @returns {Promise<Array<{key: string, value: string, frontend: boolean}>>} The settings.
|
||||
*
|
||||
* @example
|
||||
* const settings = await store.companysetting.list(42, { frontendOnly: true });
|
||||
* // => [{ key, value, frontend }]
|
||||
*/
|
||||
return async (googleAuthId, { frontendOnly = false } = {}) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -2,20 +2,24 @@ import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../sql/companysetting.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import shapeSetting from './shapeSetting.js';
|
||||
|
||||
/**
|
||||
* Sets (upserts) a company setting by key.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets a company setting.
|
||||
*
|
||||
* @example
|
||||
* const fn = set(pool);
|
||||
* const setting = await fn(42, { key: 'theme', value: 'dark', frontend: true });
|
||||
* // => { key, value, frontend }
|
||||
*/
|
||||
export default function set(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Sets (upserts) a company setting by key for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The setting input.
|
||||
* @param {string} input.key - The setting key.
|
||||
* @param {string} input.value - The setting value.
|
||||
* @param {boolean} [input.frontend] - Whether the setting is frontend-visible.
|
||||
* @returns {Promise<{key: string, value: string, frontend: boolean}>} The upserted setting.
|
||||
* @throws {Error} If the input is invalid.
|
||||
*
|
||||
* @example
|
||||
* const setting = await store.companysetting.set(42, { key: 'theme', value: 'dark', frontend: true });
|
||||
* // => { key, value, frontend }
|
||||
*/
|
||||
return async (googleAuthId, input = {}) => {
|
||||
const key = input.key?.trim();
|
||||
if (!key) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { GET_BY_ID } from '../../sql/google_auth.js';
|
||||
import resolveCompanyAccess from './resolveCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Resolves the user's selected company and checks company membership.
|
||||
* @param {object} pool
|
||||
* @returns {(googleAuthId: number) => Promise<object>}
|
||||
*/
|
||||
export default function resolveSelectedCompanyAccess(pool) {
|
||||
const access = resolveCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Resolves the user's selected company and checks company membership.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<{companyId: number, rootCategoryId: number, languageId: number, posCustomerGroupId: number|null, companyuserId: number}>} The resolved company access context.
|
||||
* @throws {Error} If the user is not authorized or has no company selected.
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
if (!Number.isFinite(googleAuthId)) {
|
||||
throw new Error('Not authorized');
|
||||
|
||||
@@ -8,20 +8,22 @@ function shape(row) {
|
||||
return { id: row.id, name: row.name, sort: row.sort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new customer group for a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates a customer group.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const group = await fn(42, { name: 'Wholesale' });
|
||||
* // => { id, name, sort }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates a new customer group for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The customer group input.
|
||||
* @param {string} input.name - The customer group name (must not be a reserved name).
|
||||
* @returns {Promise<{id: number, name: string, sort: number}>} The created customer group.
|
||||
* @throws {Error} If the name is empty or reserved.
|
||||
*
|
||||
* @example
|
||||
* const group = await store.customergroup.create(42, { name: 'Wholesale' });
|
||||
* // => { id, name, sort }
|
||||
*/
|
||||
return async (googleAuthId, { name } = {}) => {
|
||||
const cleaned = name?.trim();
|
||||
if (!cleaned) {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_COMPANY_AND_ID } from '../../sql/customergroup.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a customer group for a company by its id.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a customer group.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a customer group for the user's selected company by its id.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the customer group to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the customer group is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.customergroup.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Customer group not found');
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { LIST_BY_COMPANY } from '../../sql/customergroup.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Lists the customer groups of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists customer groups.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const groups = await fn(42);
|
||||
* // => [{ id, name, sort }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the customer groups of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, name: string, sort: number}>>} The customer groups.
|
||||
*
|
||||
* @example
|
||||
* const groups = await store.customergroup.list(42);
|
||||
* // => [{ id, name, sort }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -5,20 +5,22 @@ import {
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Reorders the customer groups of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that reorders customer groups.
|
||||
*
|
||||
* @example
|
||||
* const fn = order(pool);
|
||||
* const result = await fn(42, { customergroupIds: [3, 1, 2] });
|
||||
* // => { customergroupIds: [3, 1, 2] }
|
||||
*/
|
||||
export default function order(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Reorders the customer groups of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The reorder input.
|
||||
* @param {Array<number>} input.customergroupIds - The customer group ids in their new order.
|
||||
* @returns {Promise<{customergroupIds: Array<number>}>} The applied order.
|
||||
* @throws {Error} If the order is invalid.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.customergroup.order(42, { customergroupIds: [3, 1, 2] });
|
||||
* // => { customergroupIds: [3, 1, 2] }
|
||||
*/
|
||||
return async (googleAuthId, { customergroupIds } = {}) => {
|
||||
if (!Array.isArray(customergroupIds)) {
|
||||
throw new Error('Invalid customer group order');
|
||||
|
||||
@@ -8,20 +8,23 @@ function shape(row) {
|
||||
return { id: row.id, name: row.name, sort: row.sort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name of a customer group for a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates a customer group.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const group = await fn(42, 5, { name: 'Retail' });
|
||||
* // => { id, name, sort }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Updates the name of a customer group for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the customer group to update.
|
||||
* @param {object} input - The update input.
|
||||
* @param {string} input.name - The new customer group name.
|
||||
* @returns {Promise<{id: number, name: string, sort: number}>} The updated customer group.
|
||||
* @throws {Error} If the customer group is not found or the name is empty.
|
||||
*
|
||||
* @example
|
||||
* const group = await store.customergroup.update(42, 5, { name: 'Retail' });
|
||||
* // => { id, name, sort }
|
||||
*/
|
||||
return async (googleAuthId, id, { name } = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Customer group not found');
|
||||
|
||||
@@ -2,31 +2,33 @@ import { CREATE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import {
|
||||
loadDelivery,
|
||||
normalizeLines,
|
||||
parseDeliveryDate,
|
||||
} from './helpers.js';
|
||||
import { assertSupplier, insertLines } from './writeLines.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { loadDelivery } from './helpers.js';
|
||||
import { assertSupplier } from './writeLines.js';
|
||||
|
||||
/**
|
||||
* Creates a new delivery for a company with its lines.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates a delivery.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const delivery = await fn(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Creates a new delivery for the user's selected company with its lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} body - The delivery input.
|
||||
* @param {string} body.number - The delivery number.
|
||||
* @param {string} body.date - The delivery date.
|
||||
* @param {number} body.supplierId - The id of the supplier.
|
||||
* @param {Array<object>} [body.lines] - The delivery lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array<object>}>} The created delivery.
|
||||
* @throws {Error} If the delivery is invalid.
|
||||
*
|
||||
* @example
|
||||
* const delivery = await store.delivery.create(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, body = {}) => {
|
||||
const number = body.number?.trim?.() ? body.number.trim() : null;
|
||||
const date = parseDeliveryDate(body.date);
|
||||
const date = parseDate(body.date);
|
||||
if (!number || !date) {
|
||||
throw new Error('Invalid delivery');
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a delivery by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a delivery.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a delivery by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the delivery to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the delivery is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.delivery.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Delivery not found');
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { loadDelivery } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Returns a single delivery by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a delivery by id.
|
||||
*
|
||||
* @example
|
||||
* const fn = getById(pool);
|
||||
* const delivery = await fn(42, 5);
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
export default function getById(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a single delivery by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the delivery.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array<object>}>} The delivery.
|
||||
* @throws {Error} If the delivery is not found.
|
||||
*
|
||||
* @example
|
||||
* const delivery = await store.delivery.getById(42, 5);
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Delivery not found');
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
GET_DELIVERY_LINES,
|
||||
} from '../../sql/delivery.js';
|
||||
import request from '../request.js';
|
||||
import { shapeLine } from '../manifestLines.js';
|
||||
|
||||
export function shapeSummary(row) {
|
||||
return {
|
||||
@@ -15,19 +16,6 @@ export function shapeSummary(row) {
|
||||
};
|
||||
}
|
||||
|
||||
export function shapeLine(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
articleId: row.articleId ?? null,
|
||||
sku: row.sku ?? null,
|
||||
amount: row.amount,
|
||||
net: row.net,
|
||||
rebate: row.rebate,
|
||||
tax: row.tax,
|
||||
comment: row.comment ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadDelivery(pool, tx, companyId, id) {
|
||||
const header = await request(pool, tx)
|
||||
.input('id', id)
|
||||
@@ -49,50 +37,3 @@ export async function loadDelivery(pool, tx, companyId, id) {
|
||||
lines: lines.recordset.map(shapeLine),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseDeliveryDate(value) {
|
||||
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const d = new Date(value);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeLines(lines) {
|
||||
if (!Array.isArray(lines)) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return lines.map((entry) => {
|
||||
const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null;
|
||||
if (!sku) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
const amount = entry?.amount;
|
||||
const net = entry?.net;
|
||||
const rebate =
|
||||
entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate;
|
||||
const tax = entry?.tax;
|
||||
const comment =
|
||||
entry?.comment === undefined || entry?.comment === null
|
||||
? null
|
||||
: String(entry.comment);
|
||||
|
||||
if (
|
||||
!Number.isFinite(amount) ||
|
||||
!Number.isFinite(net) ||
|
||||
!Number.isFinite(rebate) ||
|
||||
!Number.isFinite(tax)
|
||||
) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return { sku, amount, net, rebate, tax, comment };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,20 +2,19 @@ import { DELIVERY_TYPE, LIST_DELIVERIES } from '../../sql/delivery.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { shapeSummary } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Lists the deliveries of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists deliveries.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const deliveries = await fn(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the deliveries of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, number: string, type: string, entityId: number, date: string}>>} The deliveries.
|
||||
*
|
||||
* @example
|
||||
* const deliveries = await store.delivery.list(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -7,29 +7,31 @@ import {
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import {
|
||||
loadDelivery,
|
||||
normalizeLines,
|
||||
parseDeliveryDate,
|
||||
shapeSummary,
|
||||
} from './helpers.js';
|
||||
import { assertSupplier, insertLines } from './writeLines.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { loadDelivery, shapeSummary } from './helpers.js';
|
||||
import { assertSupplier } from './writeLines.js';
|
||||
|
||||
/**
|
||||
* Updates a delivery's header and/or lines.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates a delivery.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const delivery = await fn(42, 5, { number: 'D-2', lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Updates a delivery's header and/or lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the delivery to update.
|
||||
* @param {object} patch - The update patch.
|
||||
* @param {string} [patch.number] - The new delivery number.
|
||||
* @param {string} [patch.date] - The new delivery date.
|
||||
* @param {number} [patch.supplierId] - The new supplier id.
|
||||
* @param {Array<object>} [patch.lines] - The new delivery lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array<object>}>} The updated delivery.
|
||||
* @throws {Error} If the delivery is not found.
|
||||
*
|
||||
* @example
|
||||
* const delivery = await store.delivery.update(42, 5, { number: 'D-2', lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Delivery not found');
|
||||
@@ -81,7 +83,7 @@ export default function update(pool) {
|
||||
|
||||
let date = summary.date;
|
||||
if (hasDate) {
|
||||
date = parseDeliveryDate(patch.date);
|
||||
date = parseDate(patch.date);
|
||||
if (!date) {
|
||||
throw new Error('Invalid delivery');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
CREATE_LINE,
|
||||
RESOLVE_ARTICLE_ID_BY_SKU,
|
||||
} from '../../sql/delivery.js';
|
||||
import { GET_SUPPLIER_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
|
||||
import request from '../request.js';
|
||||
|
||||
@@ -24,42 +20,3 @@ export async function assertSupplier(pool, tx, companyId, supplierId) {
|
||||
}
|
||||
return supplierId;
|
||||
}
|
||||
|
||||
async function resolveArticleId(pool, tx, rootCategoryId, sku) {
|
||||
const result = await request(pool, tx)
|
||||
.input('rootCategoryId', rootCategoryId)
|
||||
.input('sku', sku)
|
||||
.query(RESOLVE_ARTICLE_ID_BY_SKU);
|
||||
|
||||
const articleId = result.recordset[0]?.id;
|
||||
if (!articleId) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
return articleId;
|
||||
}
|
||||
|
||||
export async function insertLines(
|
||||
pool,
|
||||
tx,
|
||||
{ manifestId, rootCategoryId, lines, ensureTax },
|
||||
) {
|
||||
for (const line of lines) {
|
||||
const articleId = await resolveArticleId(
|
||||
pool,
|
||||
tx,
|
||||
rootCategoryId,
|
||||
line.sku,
|
||||
);
|
||||
const taxId = await ensureTax(line.tax, tx);
|
||||
|
||||
await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.input('articleId', articleId)
|
||||
.input('amount', line.amount)
|
||||
.input('net', line.net)
|
||||
.input('comment', line.comment)
|
||||
.input('rebate', line.rebate)
|
||||
.input('taxId', taxId)
|
||||
.query(CREATE_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,29 @@ import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAcc
|
||||
import nextNumber from '../numbers/nextNumber.js';
|
||||
import shape from './shape.js';
|
||||
|
||||
/**
|
||||
* Creates a new entity (customer) in a customer group, recording history.
|
||||
* Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding`
|
||||
* (`type` `entity`).
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates an entity.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const entity = await fn(42, { customergroupId: 5, name: 'Doe', firstName: 'John' });
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const groupAccess = resolveCustomerGroupAccess(pool);
|
||||
const allocateNumber = nextNumber(pool);
|
||||
|
||||
/**
|
||||
* Creates a new entity (customer) in a customer group, recording history.
|
||||
* Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding`
|
||||
* (`type` `entity`).
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The entity creation input.
|
||||
* @param {number} input.customergroupId - The id of the customer group.
|
||||
* @param {string} input.name - The entity name.
|
||||
* @param {string} [input.firstName] - The entity first name.
|
||||
* @param {string} [input.number] - Optional explicit customer number.
|
||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The created entity.
|
||||
* @throws {Error} If the name is empty.
|
||||
*
|
||||
* @example
|
||||
* const entity = await store.entity.create(42, { customergroupId: 5, name: 'Doe', firstName: 'John' });
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
return async (googleAuthId, { customergroupId, name, firstName = null, number } = {}) => {
|
||||
const cleaned = name?.trim();
|
||||
if (!cleaned) {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_COMPANY_AND_ID } from '../../sql/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes an entity by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes an entity.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes an entity by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the entity to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the entity is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.entity.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Entity not found');
|
||||
|
||||
@@ -2,20 +2,21 @@ import { GET_BY_COMPANY_AND_ID } from '../../sql/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import shape from './shape.js';
|
||||
|
||||
/**
|
||||
* Returns a single entity by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches an entity by id.
|
||||
*
|
||||
* @example
|
||||
* const fn = getById(pool);
|
||||
* const entity = await fn(42, 5);
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
export default function getById(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a single entity by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the entity.
|
||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The entity.
|
||||
* @throws {Error} If the entity is not found.
|
||||
*
|
||||
* @example
|
||||
* const entity = await store.entity.getById(42, 5);
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Entity not found');
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { NUMBER_TAKEN_IN_COMPANY } from '../../sql/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Returns whether a customer number is free within the company.
|
||||
* Pass `excludeEntityId` when editing so the entity's own number counts as free.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that checks number availability.
|
||||
*
|
||||
* @example
|
||||
* const fn = isNumberFree(pool);
|
||||
* const free = await fn(42, 'C-100');
|
||||
* // => true
|
||||
* const freeOnEdit = await fn(42, 'C-100', { excludeEntityId: 5 });
|
||||
* // => true
|
||||
*/
|
||||
export default function isNumberFree(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns whether a customer number is free within the user's selected company.
|
||||
* Pass `excludeEntityId` when editing so the entity's own number counts as free.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} number - The customer number to check.
|
||||
* @param {object} [options] - Optional options.
|
||||
* @param {number} [options.excludeEntityId] - An entity id to exclude from the check.
|
||||
* @returns {Promise<boolean>} Whether the number is free.
|
||||
* @throws {Error} If the number is empty.
|
||||
*
|
||||
* @example
|
||||
* const free = await store.entity.isNumberFree(42, 'C-100');
|
||||
* // => true
|
||||
* const freeOnEdit = await store.entity.isNumberFree(42, 'C-100', { excludeEntityId: 5 });
|
||||
* // => true
|
||||
*/
|
||||
return async (googleAuthId, number, { excludeEntityId } = {}) => {
|
||||
const cleaned =
|
||||
number == null || number === '' ? null : String(number).trim() || null;
|
||||
|
||||
@@ -9,22 +9,25 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
|
||||
import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js';
|
||||
import shape from './shape.js';
|
||||
|
||||
/**
|
||||
* Lists the entities (customers) of a company, optionally filtered by customer
|
||||
* group or restricted to the 'customer' role.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists entities.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const entities = await fn(42, { customergroupId: 5 });
|
||||
* // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const groupAccess = resolveCustomerGroupAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the entities (customers) of the user's selected company, optionally
|
||||
* filtered by customer group or restricted to the 'customer' role.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} [options] - Optional options.
|
||||
* @param {number} [options.customergroupId] - Filter by customer group id.
|
||||
* @param {string} [options.role] - Restrict to `'customer'` role if set.
|
||||
* @returns {Promise<Array<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>>} The entities.
|
||||
* @throws {Error} If the role is invalid.
|
||||
*
|
||||
* @example
|
||||
* const entities = await store.entity.list(42, { customergroupId: 5 });
|
||||
* // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }]
|
||||
*/
|
||||
return async (googleAuthId, { customergroupId, role } = {}) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import peekNumber from '../numbers/peekNumber.js';
|
||||
|
||||
/**
|
||||
* Returns the next customer number that would be allocated for the company
|
||||
* (`numbers` type `entity`) without consuming it.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that peeks the next entity number.
|
||||
*
|
||||
* @example
|
||||
* const fn = peekNextNumber(pool);
|
||||
* const number = await fn(42);
|
||||
* // => '1'
|
||||
*/
|
||||
export default function peekNextNumber(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const peek = peekNumber(pool);
|
||||
|
||||
/**
|
||||
* Returns the next customer number that would be allocated for the user's
|
||||
* selected company (`numbers` type `entity`) without consuming it.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<string>} The next entity number.
|
||||
*
|
||||
* @example
|
||||
* const number = await store.entity.peekNextNumber(42);
|
||||
* // => '1'
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
return peek(companyId, 'entity');
|
||||
|
||||
@@ -4,21 +4,26 @@ import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import shape from './shape.js';
|
||||
|
||||
/**
|
||||
* Updates name, first name, and/or customer number of an entity, recording history.
|
||||
* Optional `number` sets the customer number explicitly (does not auto-allocate).
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates an entity.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const entity = await fn(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' });
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Updates name, first name, and/or customer number of an entity, recording history.
|
||||
* Optional `number` sets the customer number explicitly (does not auto-allocate).
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the entity to update.
|
||||
* @param {object} patch - The update patch.
|
||||
* @param {string} [patch.name] - The new entity name.
|
||||
* @param {string} [patch.firstName] - The new first name.
|
||||
* @param {string} [patch.number] - The new customer number.
|
||||
* @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The updated entity.
|
||||
* @throws {Error} If no valid field is provided or the entity is not found.
|
||||
*
|
||||
* @example
|
||||
* const entity = await store.entity.update(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' });
|
||||
* // => { id, customergroupId, firstName, name, number, createdAt, updatedAt }
|
||||
*/
|
||||
return async (googleAuthId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Entity not found');
|
||||
|
||||
@@ -24,21 +24,32 @@ function optionalString(value) {
|
||||
return s || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new address for an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates an entity address.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const address = await fn(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' });
|
||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates a new address for an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {object} input - The address input.
|
||||
* @param {string} input.addressType - The address type (e.g. `'billing'`).
|
||||
* @param {string} [input.firstName] - The first name.
|
||||
* @param {string} [input.name] - The name.
|
||||
* @param {string} [input.street] - The street.
|
||||
* @param {string} [input.streetNumber] - The street number.
|
||||
* @param {string} [input.zip] - The zip code.
|
||||
* @param {string} [input.city] - The city.
|
||||
* @param {string} [input.county] - The county.
|
||||
* @param {number} [input.sort] - Optional sort order.
|
||||
* @returns {Promise<object>} The created address.
|
||||
* @throws {Error} If the address type is missing.
|
||||
*
|
||||
* @example
|
||||
* const address = await store.entityaddress.create(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' });
|
||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||
*/
|
||||
return async (googleAuthId, entityId, input = {}) => {
|
||||
const addressType = input.addressType?.trim?.()
|
||||
? input.addressType.trim()
|
||||
|
||||
@@ -2,20 +2,22 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entityaddress.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes an address of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes an entity address.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5, 9);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes an address of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {number} id - The id of the address to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the address is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.entityaddress.delete(42, 5, 9);
|
||||
*/
|
||||
return async (googleAuthId, entityId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Address not found');
|
||||
|
||||
@@ -18,21 +18,21 @@ function shape(row) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the addresses of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists entity addresses.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const addresses = await fn(42, 5);
|
||||
* // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the addresses of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @returns {Promise<Array<object>>} The addresses.
|
||||
*
|
||||
* @example
|
||||
* const addresses = await store.entityaddress.list(42, 5);
|
||||
* // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }]
|
||||
*/
|
||||
return async (googleAuthId, entityId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
await entityAccess(companyId, entityId);
|
||||
|
||||
@@ -24,21 +24,24 @@ function optionalString(value) {
|
||||
return s || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an address of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates an entity address.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const address = await fn(42, 5, 9, { city: 'Hamburg' });
|
||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Updates an address of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {number} id - The id of the address to update.
|
||||
* @param {object} patch - The update patch (any subset of address fields).
|
||||
* @returns {Promise<object>} The updated address.
|
||||
* @throws {Error} If the address is not found.
|
||||
*
|
||||
* @example
|
||||
* const address = await store.entityaddress.update(42, 5, 9, { city: 'Hamburg' });
|
||||
* // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }
|
||||
*/
|
||||
return async (googleAuthId, entityId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Address not found');
|
||||
|
||||
@@ -12,21 +12,26 @@ function shape(row) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new contact for an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates an entity contact.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' });
|
||||
* // => { id, entityId, contactType, contact, sort }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates a new contact for an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {object} input - The contact input.
|
||||
* @param {string} input.contactType - The contact type (e.g. `'email'`).
|
||||
* @param {string} input.contact - The contact value.
|
||||
* @param {number} [input.sort] - Optional sort order.
|
||||
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The created contact.
|
||||
* @throws {Error} If the contact type or value is missing.
|
||||
*
|
||||
* @example
|
||||
* const contact = await store.entitycontact.create(42, 5, { contactType: 'email', contact: 'a@b.c' });
|
||||
* // => { id, entityId, contactType, contact, sort }
|
||||
*/
|
||||
return async (googleAuthId, entityId, input = {}) => {
|
||||
const contactType = input.contactType?.trim?.()
|
||||
? input.contactType.trim()
|
||||
|
||||
@@ -2,20 +2,22 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entitycontact.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a contact of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes an entity contact.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5, 9);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a contact of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {number} id - The id of the contact to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the contact is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.entitycontact.delete(42, 5, 9);
|
||||
*/
|
||||
return async (googleAuthId, entityId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Contact not found');
|
||||
|
||||
@@ -12,21 +12,21 @@ function shape(row) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the contacts of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists entity contacts.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const contacts = await fn(42, 5);
|
||||
* // => [{ id, entityId, contactType, contact, sort }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the contacts of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @returns {Promise<Array<{id: number, entityId: number, contactType: string, contact: string, sort: number}>>} The contacts.
|
||||
*
|
||||
* @example
|
||||
* const contacts = await store.entitycontact.list(42, 5);
|
||||
* // => [{ id, entityId, contactType, contact, sort }]
|
||||
*/
|
||||
return async (googleAuthId, entityId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
await entityAccess(companyId, entityId);
|
||||
|
||||
@@ -12,21 +12,24 @@ function shape(row) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a contact of an entity.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates an entity contact.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const contact = await fn(42, 5, 9, { contact: 'b@c.d' });
|
||||
* // => { id, entityId, contactType, contact, sort }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const entityAccess = resolveEntityAccess(pool);
|
||||
|
||||
/**
|
||||
* Updates a contact of an entity.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} entityId - The id of the entity.
|
||||
* @param {number} id - The id of the contact to update.
|
||||
* @param {object} patch - The update patch (any subset of contact fields).
|
||||
* @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The updated contact.
|
||||
* @throws {Error} If the contact is not found.
|
||||
*
|
||||
* @example
|
||||
* const contact = await store.entitycontact.update(42, 5, 9, { contact: 'b@c.d' });
|
||||
* // => { id, entityId, contactType, contact, sort }
|
||||
*/
|
||||
return async (googleAuthId, entityId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Contact not found');
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ADD } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Adds a new allowed Google auth email.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that adds an allowed email.
|
||||
*
|
||||
* @example
|
||||
* const fn = add(pool);
|
||||
* const id = await fn('user@example.com');
|
||||
* // => 1
|
||||
*/
|
||||
export default function add(pool) {
|
||||
/**
|
||||
* Adds a new allowed Google auth email.
|
||||
*
|
||||
* @param {string} email - The email address to allow.
|
||||
* @returns {Promise<number>} The id of the created Google auth row.
|
||||
*
|
||||
* @example
|
||||
* const id = await store.google_auth.add('user@example.com');
|
||||
* // => 1
|
||||
*/
|
||||
return async (email) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { GET_BY_EMAIL } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Returns a Google auth row by email.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a Google auth row by email.
|
||||
*
|
||||
* @example
|
||||
* const fn = getByEmail(pool);
|
||||
* const auth = await fn('user@example.com');
|
||||
* // => { id, email, ... }
|
||||
*/
|
||||
export default function getByEmail(pool) {
|
||||
/**
|
||||
* Returns a Google auth row by email.
|
||||
*
|
||||
* @param {string} email - The email address.
|
||||
* @returns {Promise<object|undefined>} The Google auth row, or `undefined` if not found.
|
||||
*
|
||||
* @example
|
||||
* const auth = await store.google_auth.getByEmail('user@example.com');
|
||||
* // => { id, email, ... }
|
||||
*/
|
||||
return async (email) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { GET_BY_ID } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Returns a Google auth row by its id.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a Google auth row by id.
|
||||
*
|
||||
* @example
|
||||
* const fn = getById(pool);
|
||||
* const auth = await fn(42);
|
||||
* // => { id, email, ... }
|
||||
*/
|
||||
export default function getById(pool) {
|
||||
/**
|
||||
* Returns a Google auth row by its id.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth row.
|
||||
* @returns {Promise<object|undefined>} The Google auth row, or `undefined` if not found.
|
||||
*
|
||||
* @example
|
||||
* const auth = await store.google_auth.getById(42);
|
||||
* // => { id, email, ... }
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { LIST_ALLOWED_EMAILS } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Lists all allowed Google auth emails.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists allowed emails.
|
||||
*
|
||||
* @example
|
||||
* const fn = listAllowedEmails(pool);
|
||||
* const emails = await fn();
|
||||
* // => [{ id, email }]
|
||||
*/
|
||||
export default function listAllowedEmails(pool) {
|
||||
/**
|
||||
* Lists all allowed Google auth emails.
|
||||
*
|
||||
* @returns {Promise<Array<{id: number, email: string}>>} The allowed emails.
|
||||
*
|
||||
* @example
|
||||
* const emails = await store.google_auth.listAllowedEmails();
|
||||
* // => [{ id, email }]
|
||||
*/
|
||||
return async () => {
|
||||
const result = await pool.request().query(LIST_ALLOWED_EMAILS);
|
||||
return result.recordset;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { REMOVE_ALLOWED_EMAIL } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Removes an allowed Google auth email.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that removes an allowed email.
|
||||
*
|
||||
* @example
|
||||
* const fn = removeAllowedEmail(pool);
|
||||
* await fn('user@example.com');
|
||||
*/
|
||||
export default function removeAllowedEmail(pool) {
|
||||
/**
|
||||
* Removes an allowed Google auth email.
|
||||
*
|
||||
* @param {string} email - The email address to remove.
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @example
|
||||
* await store.google_auth.removeAllowedEmail('user@example.com');
|
||||
*/
|
||||
return async (email) => {
|
||||
await pool
|
||||
.request()
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { SET_PROFILE } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Updates the profile (picture, picture binary, sub) of a Google auth user.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets the user profile.
|
||||
*
|
||||
* @example
|
||||
* const fn = setProfile(pool);
|
||||
* await fn(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' });
|
||||
*/
|
||||
export default function setProfile(pool) {
|
||||
/**
|
||||
* Updates the profile (picture, picture binary, sub) of a Google auth user.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The profile input.
|
||||
* @param {string} [input.picture] - The picture URL.
|
||||
* @param {Buffer} [input.pictureBinary] - The picture binary.
|
||||
* @param {string} [input.sub] - The Google sub identifier.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the user is not found or the picture binary is invalid.
|
||||
*
|
||||
* @example
|
||||
* await store.google_auth.setProfile(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' });
|
||||
*/
|
||||
return async (
|
||||
googleAuthId,
|
||||
{ picture = null, pictureBinary = null, sub = null } = {},
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js';
|
||||
|
||||
/**
|
||||
* Sets the currently selected company for a Google auth user.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that sets the selected company.
|
||||
*
|
||||
* @example
|
||||
* const fn = setSelectedCompany(pool);
|
||||
* await fn(42, 1);
|
||||
*/
|
||||
export default function setSelectedCompany(pool) {
|
||||
/**
|
||||
* Sets the currently selected company for a Google auth user.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} companyId - The id of the company to select.
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @example
|
||||
* await store.google_auth.setSelectedCompany(42, 1);
|
||||
*/
|
||||
return async (googleAuthId, companyId) => {
|
||||
await pool
|
||||
.request()
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { SCHEMA_SCRIPTS } from '../schema.js';
|
||||
|
||||
/**
|
||||
* Initializes the database schema by running all schema scripts.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that initializes the schema.
|
||||
*
|
||||
* @example
|
||||
* const fn = initialize(pool);
|
||||
* await fn();
|
||||
*/
|
||||
export default function initialize(pool) {
|
||||
/**
|
||||
* Initializes the database schema by running all schema scripts.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @example
|
||||
* await store.initialize();
|
||||
*/
|
||||
return async () => {
|
||||
for (const script of SCHEMA_SCRIPTS) {
|
||||
await pool.request().query(script);
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
import {
|
||||
CREATE_LINE,
|
||||
CREATE_MANIFEST,
|
||||
DELETE_LINES_BY_MANIFEST,
|
||||
DELETE_MANIFEST,
|
||||
GET_MANIFEST_HEADER,
|
||||
GET_MANIFEST_LINES,
|
||||
LIST_MANIFESTS,
|
||||
RESOLVE_ARTICLE_ID_BY_SKU,
|
||||
UPDATE_MANIFEST,
|
||||
} from '../../sql/manifestDoc.js';
|
||||
import {
|
||||
GET_CUSTOMER_BY_ID,
|
||||
POS_GROUP_NAME,
|
||||
SUPPLIER_GROUP_NAME,
|
||||
} from '../../sql/entity.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
|
||||
function shapeSummary(type, row) {
|
||||
return {
|
||||
id: row.id,
|
||||
number: row.number,
|
||||
type,
|
||||
entityId: row.entityId ?? null,
|
||||
date: row.date,
|
||||
};
|
||||
}
|
||||
|
||||
function shapeLine(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
articleId: row.articleId ?? null,
|
||||
sku: row.sku ?? null,
|
||||
amount: row.amount,
|
||||
net: row.net,
|
||||
rebate: row.rebate,
|
||||
tax: row.tax,
|
||||
comment: row.comment ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseDate(value) {
|
||||
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const d = new Date(value);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeLines(lines) {
|
||||
if (!Array.isArray(lines)) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return lines.map((entry) => {
|
||||
const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null;
|
||||
if (!sku) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
const amount = entry?.amount;
|
||||
const net = entry?.net;
|
||||
const rebate =
|
||||
entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate;
|
||||
const tax = entry?.tax;
|
||||
const comment =
|
||||
entry?.comment === undefined || entry?.comment === null
|
||||
? null
|
||||
: String(entry.comment);
|
||||
|
||||
if (
|
||||
!Number.isFinite(amount) ||
|
||||
!Number.isFinite(net) ||
|
||||
!Number.isFinite(rebate) ||
|
||||
!Number.isFinite(tax)
|
||||
) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return { sku, amount, net, rebate, tax, comment };
|
||||
});
|
||||
}
|
||||
|
||||
async function assertCustomerEntity(pool, tx, companyId, entityId) {
|
||||
if (entityId == null) {
|
||||
return null;
|
||||
}
|
||||
if (!Number.isFinite(entityId)) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
|
||||
const result = await request(pool, tx)
|
||||
.input('id', entityId)
|
||||
.input('companyId', companyId)
|
||||
.input('supplierGroupName', SUPPLIER_GROUP_NAME)
|
||||
.input('posGroupName', POS_GROUP_NAME)
|
||||
.query(GET_CUSTOMER_BY_ID);
|
||||
|
||||
if (!result.recordset[0]) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
return entityId;
|
||||
}
|
||||
|
||||
async function resolveArticleId(pool, tx, rootCategoryId, sku) {
|
||||
const result = await request(pool, tx)
|
||||
.input('rootCategoryId', rootCategoryId)
|
||||
.input('sku', sku)
|
||||
.query(RESOLVE_ARTICLE_ID_BY_SKU);
|
||||
|
||||
const articleId = result.recordset[0]?.id;
|
||||
if (!articleId) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
return articleId;
|
||||
}
|
||||
|
||||
async function insertLines(
|
||||
pool,
|
||||
tx,
|
||||
{ manifestId, rootCategoryId, lines, ensureTax },
|
||||
) {
|
||||
for (const line of lines) {
|
||||
const articleId = await resolveArticleId(
|
||||
pool,
|
||||
tx,
|
||||
rootCategoryId,
|
||||
line.sku,
|
||||
);
|
||||
const taxId = await ensureTax(line.tax, tx);
|
||||
|
||||
await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.input('articleId', articleId)
|
||||
.input('amount', line.amount)
|
||||
.input('net', line.net)
|
||||
.input('comment', line.comment)
|
||||
.input('rebate', line.rebate)
|
||||
.input('taxId', taxId)
|
||||
.query(CREATE_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDoc(pool, tx, { type, companyId, id }) {
|
||||
const header = await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.query(GET_MANIFEST_HEADER);
|
||||
|
||||
const row = header.recordset[0];
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = await request(pool, tx)
|
||||
.input('manifestId', id)
|
||||
.query(GET_MANIFEST_LINES);
|
||||
|
||||
return {
|
||||
...shapeSummary(type, row),
|
||||
lines: lines.recordset.map(shapeLine),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for quote / order (and similar) stores on manifest / manifestarticle.
|
||||
*
|
||||
* @param {object} pool
|
||||
* @param {{ type: string, label: string }} options
|
||||
* `type` → manifest.type (`quote` / `order`)
|
||||
* `label` → error prefix (`Quote` / `Order` → "Quote not found")
|
||||
*/
|
||||
export default function createManifestDocStore(pool, { type, label }) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
const notFound = `${label} not found`;
|
||||
const invalid = `Invalid ${label.toLowerCase()}`;
|
||||
|
||||
/**
|
||||
* Lists the quote/order documents of a company.
|
||||
*
|
||||
* @param {number} googleAuthId - The Google auth user id.
|
||||
* @returns {Promise<Array<object>>} The document summaries.
|
||||
*/
|
||||
async function list(googleAuthId) {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.query(LIST_MANIFESTS);
|
||||
|
||||
return result.recordset.map((row) => shapeSummary(type, row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single quote/order document by its id within a company.
|
||||
*
|
||||
* @param {number} googleAuthId - The Google auth user id.
|
||||
* @param {number} id - The document id.
|
||||
* @returns {Promise<object|null>} The document with its lines, or null if not found.
|
||||
*/
|
||||
async function getById(googleAuthId, id) {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const doc = await loadDoc(pool, null, { type, companyId, id });
|
||||
if (!doc) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new quote/order document with its lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The Google auth user id.
|
||||
* @param {object} body - The document payload (`number`, `date`, `entityId`, `lines`).
|
||||
* @returns {Promise<object>} The created document with its lines.
|
||||
*/
|
||||
async function create(googleAuthId, body = {}) {
|
||||
const number = body.number?.trim?.() ? body.number.trim() : null;
|
||||
const date = parseDate(body.date);
|
||||
if (!number || !date) {
|
||||
throw new Error(invalid);
|
||||
}
|
||||
|
||||
const lines = normalizeLines(body.lines ?? []);
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
body.entityId ?? null,
|
||||
);
|
||||
|
||||
const created = await request(pool, tx)
|
||||
.input('companyId', companyId)
|
||||
.input('number', number)
|
||||
.input('type', type)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(CREATE_MANIFEST);
|
||||
|
||||
const manifestId = created.recordset[0].id;
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId,
|
||||
rootCategoryId,
|
||||
lines,
|
||||
ensureTax,
|
||||
});
|
||||
|
||||
const doc = await loadDoc(pool, tx, { type, companyId, id: manifestId });
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a quote/order document's header and/or lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The Google auth user id.
|
||||
* @param {number} id - The document id.
|
||||
* @param {object} patch - The fields to update (`number`, `date`, `entityId`, `lines`).
|
||||
* @returns {Promise<object>} The updated document with its lines.
|
||||
*/
|
||||
async function update(googleAuthId, id, patch = {}) {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
|
||||
const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number');
|
||||
const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date');
|
||||
const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId');
|
||||
const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines');
|
||||
|
||||
if (!hasNumber && !hasDate && !hasEntityId && !hasLines) {
|
||||
throw new Error(invalid);
|
||||
}
|
||||
|
||||
let normalizedLines;
|
||||
if (hasLines) {
|
||||
normalizedLines = normalizeLines(patch.lines);
|
||||
}
|
||||
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const existing = await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.query(GET_MANIFEST_HEADER);
|
||||
|
||||
const current = existing.recordset[0];
|
||||
if (!current) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
|
||||
const summary = shapeSummary(type, current);
|
||||
|
||||
let number = summary.number;
|
||||
if (hasNumber) {
|
||||
number = patch.number?.trim?.() ? patch.number.trim() : null;
|
||||
if (!number) {
|
||||
throw new Error(invalid);
|
||||
}
|
||||
}
|
||||
|
||||
let date = summary.date;
|
||||
if (hasDate) {
|
||||
date = parseDate(patch.date);
|
||||
if (!date) {
|
||||
throw new Error(invalid);
|
||||
}
|
||||
}
|
||||
|
||||
let entityId = summary.entityId;
|
||||
if (hasEntityId) {
|
||||
entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
patch.entityId ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.input('number', number)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(UPDATE_MANIFEST);
|
||||
|
||||
if (hasLines) {
|
||||
await request(pool, tx)
|
||||
.input('manifestId', id)
|
||||
.query(DELETE_LINES_BY_MANIFEST);
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId: id,
|
||||
rootCategoryId,
|
||||
lines: normalizedLines,
|
||||
ensureTax,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = await loadDoc(pool, tx, { type, companyId, id });
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a quote/order document by its id within a company.
|
||||
*
|
||||
* @param {number} googleAuthId - The Google auth user id.
|
||||
* @param {number} id - The document id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function del(googleAuthId, id) {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.query(DELETE_MANIFEST);
|
||||
|
||||
if (!result.rowsAffected[0]) {
|
||||
throw new Error(notFound);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
getById,
|
||||
create,
|
||||
update,
|
||||
delete: del,
|
||||
};
|
||||
}
|
||||
64
src/store/manifestDoc/helpers.js
Normal file
64
src/store/manifestDoc/helpers.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
GET_MANIFEST_HEADER,
|
||||
GET_MANIFEST_LINES,
|
||||
} from '../../sql/manifestDoc.js';
|
||||
import {
|
||||
GET_CUSTOMER_BY_ID,
|
||||
POS_GROUP_NAME,
|
||||
SUPPLIER_GROUP_NAME,
|
||||
} from '../../sql/entity.js';
|
||||
import request from '../request.js';
|
||||
import { shapeLine } from '../manifestLines.js';
|
||||
|
||||
export function shapeSummary(type, row) {
|
||||
return {
|
||||
id: row.id,
|
||||
number: row.number,
|
||||
type,
|
||||
entityId: row.entityId ?? null,
|
||||
date: row.date,
|
||||
};
|
||||
}
|
||||
|
||||
export async function assertCustomerEntity(pool, tx, companyId, entityId) {
|
||||
if (entityId == null) {
|
||||
return null;
|
||||
}
|
||||
if (!Number.isFinite(entityId)) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
|
||||
const result = await request(pool, tx)
|
||||
.input('id', entityId)
|
||||
.input('companyId', companyId)
|
||||
.input('supplierGroupName', SUPPLIER_GROUP_NAME)
|
||||
.input('posGroupName', POS_GROUP_NAME)
|
||||
.query(GET_CUSTOMER_BY_ID);
|
||||
|
||||
if (!result.recordset[0]) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
return entityId;
|
||||
}
|
||||
|
||||
export async function loadDoc(pool, tx, { type, companyId, id }) {
|
||||
const header = await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', type)
|
||||
.query(GET_MANIFEST_HEADER);
|
||||
|
||||
const row = header.recordset[0];
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = await request(pool, tx)
|
||||
.input('manifestId', id)
|
||||
.query(GET_MANIFEST_LINES);
|
||||
|
||||
return {
|
||||
...shapeSummary(type, row),
|
||||
lines: lines.recordset.map(shapeLine),
|
||||
};
|
||||
}
|
||||
133
src/store/manifestLines.js
Normal file
133
src/store/manifestLines.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { CREATE_LINE, RESOLVE_ARTICLE_ID_BY_SKU } from '../sql/manifestDoc.js';
|
||||
import request from './request.js';
|
||||
|
||||
/**
|
||||
* Normalizes and validates an array of manifest line inputs.
|
||||
*
|
||||
* @param {Array<object>} lines - The raw line inputs.
|
||||
* @returns {Array<{sku: string, amount: number, net: number, rebate: number, tax: number, comment: string|null}>} The normalized lines.
|
||||
* @throws {Error} If the lines are invalid.
|
||||
*/
|
||||
export function normalizeLines(lines) {
|
||||
if (!Array.isArray(lines)) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return lines.map((entry) => {
|
||||
const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null;
|
||||
if (!sku) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
const amount = entry?.amount;
|
||||
const net = entry?.net;
|
||||
const rebate =
|
||||
entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate;
|
||||
const tax = entry?.tax;
|
||||
const comment =
|
||||
entry?.comment === undefined || entry?.comment === null
|
||||
? null
|
||||
: String(entry.comment);
|
||||
|
||||
if (
|
||||
!Number.isFinite(amount) ||
|
||||
!Number.isFinite(net) ||
|
||||
!Number.isFinite(rebate) ||
|
||||
!Number.isFinite(tax)
|
||||
) {
|
||||
throw new Error('Invalid lines');
|
||||
}
|
||||
|
||||
return { sku, amount, net, rebate, tax, comment };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a date value (Date instance or ISO string) into a Date.
|
||||
*
|
||||
* @param {Date|string} value - The date value.
|
||||
* @returns {Date|null} The parsed date, or null if invalid.
|
||||
*/
|
||||
export function parseDate(value) {
|
||||
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const d = new Date(value);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shapes a manifestarticle row into a line object.
|
||||
*
|
||||
* @param {object} row - The manifestarticle row.
|
||||
* @returns {object} The shaped line.
|
||||
*/
|
||||
export function shapeLine(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
articleId: row.articleId ?? null,
|
||||
sku: row.sku ?? null,
|
||||
amount: row.amount,
|
||||
net: row.net,
|
||||
rebate: row.rebate,
|
||||
tax: row.tax,
|
||||
comment: row.comment ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveArticleId(pool, tx, rootCategoryId, sku) {
|
||||
const result = await request(pool, tx)
|
||||
.input('rootCategoryId', rootCategoryId)
|
||||
.input('sku', sku)
|
||||
.query(RESOLVE_ARTICLE_ID_BY_SKU);
|
||||
|
||||
const articleId = result.recordset[0]?.id;
|
||||
if (!articleId) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
return articleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts manifest lines for a document, resolving each SKU to an article id
|
||||
* and ensuring the tax rate.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @param {object} tx - The transaction.
|
||||
* @param {object} params - The insert parameters.
|
||||
* @param {number} params.manifestId - The manifest id.
|
||||
* @param {number} params.rootCategoryId - The company root category id.
|
||||
* @param {Array<object>} params.lines - The normalized lines.
|
||||
* @param {Function} params.ensureTax - The tax ensure function.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function insertLines(
|
||||
pool,
|
||||
tx,
|
||||
{ manifestId, rootCategoryId, lines, ensureTax },
|
||||
) {
|
||||
for (const line of lines) {
|
||||
const articleId = await resolveArticleId(
|
||||
pool,
|
||||
tx,
|
||||
rootCategoryId,
|
||||
line.sku,
|
||||
);
|
||||
const taxId = await ensureTax(line.tax, tx);
|
||||
|
||||
await request(pool, tx)
|
||||
.input('manifestId', manifestId)
|
||||
.input('articleId', articleId)
|
||||
.input('amount', line.amount)
|
||||
.input('net', line.net)
|
||||
.input('comment', line.comment)
|
||||
.input('rebate', line.rebate)
|
||||
.input('taxId', taxId)
|
||||
.query(CREATE_LINE);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,18 @@ import { ENSURE_TYPE } from '../../sql/numbers.js';
|
||||
import request from '../request.js';
|
||||
|
||||
export default function nextNumber(pool) {
|
||||
/**
|
||||
* Allocates and returns the next number for a company and type, consuming it.
|
||||
*
|
||||
* @param {number} companyId - The id of the company.
|
||||
* @param {string} type - The number type (e.g. `'entity'`, `'sku'`).
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<string>} The allocated number.
|
||||
*
|
||||
* @example
|
||||
* const number = await store.numbers.next(1, 'entity');
|
||||
* // => '1'
|
||||
*/
|
||||
return async (companyId, type, tx) => {
|
||||
await request(pool, tx)
|
||||
.input('companyId', companyId)
|
||||
|
||||
@@ -2,5 +2,16 @@ import nextNumber from './nextNumber.js';
|
||||
|
||||
export default function nextSku(pool) {
|
||||
const allocate = nextNumber(pool);
|
||||
/**
|
||||
* Allocates and returns the next SKU for a company, consuming it.
|
||||
*
|
||||
* @param {number} companyId - The id of the company.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<string>} The allocated SKU.
|
||||
*
|
||||
* @example
|
||||
* const sku = await store.numbers.nextSku(1);
|
||||
* // => '1'
|
||||
*/
|
||||
return (companyId, tx) => allocate(companyId, 'sku', tx);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,18 @@ import { ENSURE_TYPE } from '../../sql/numbers.js';
|
||||
import request from '../request.js';
|
||||
|
||||
export default function peekNumber(pool) {
|
||||
/**
|
||||
* Returns the next number for a company and type without consuming it.
|
||||
*
|
||||
* @param {number} companyId - The id of the company.
|
||||
* @param {string} type - The number type (e.g. `'entity'`, `'sku'`).
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<string>} The next number.
|
||||
*
|
||||
* @example
|
||||
* const number = await store.numbers.peek(1, 'entity');
|
||||
* // => '1'
|
||||
*/
|
||||
return async (companyId, type, tx) => {
|
||||
await request(pool, tx)
|
||||
.input('companyId', companyId)
|
||||
|
||||
82
src/store/order/create.js
Normal file
82
src/store/order/create.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { CREATE_MANIFEST, ORDER_TYPE } from '../../sql/manifestDoc.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Creates a new order for the user's selected company with its lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} body - The order input.
|
||||
* @param {string} body.number - The order number.
|
||||
* @param {string} body.date - The order date.
|
||||
* @param {number} [body.entityId] - The id of the customer entity.
|
||||
* @param {Array<object>} [body.lines] - The order lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The created order.
|
||||
* @throws {Error} If the order is invalid.
|
||||
*
|
||||
* @example
|
||||
* const order = await store.order.create(42, { number: 'B-2026-001', date: '2026-09-15', entityId: 12, lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, body = {}) => {
|
||||
const number = body.number?.trim?.() ? body.number.trim() : null;
|
||||
const date = parseDate(body.date);
|
||||
if (!number || !date) {
|
||||
throw new Error('Invalid order');
|
||||
}
|
||||
|
||||
const lines = normalizeLines(body.lines ?? []);
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
body.entityId ?? null,
|
||||
);
|
||||
|
||||
const created = await request(pool, tx)
|
||||
.input('companyId', companyId)
|
||||
.input('number', number)
|
||||
.input('type', ORDER_TYPE)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(CREATE_MANIFEST);
|
||||
|
||||
const manifestId = created.recordset[0].id;
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId,
|
||||
rootCategoryId,
|
||||
lines,
|
||||
ensureTax,
|
||||
});
|
||||
|
||||
const doc = await loadDoc(pool, tx, {
|
||||
type: ORDER_TYPE,
|
||||
companyId,
|
||||
id: manifestId,
|
||||
});
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
36
src/store/order/delete.js
Normal file
36
src/store/order/delete.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { DELETE_MANIFEST, ORDER_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes an order by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the order to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the order is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.order.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', ORDER_TYPE)
|
||||
.query(DELETE_MANIFEST);
|
||||
|
||||
if (!result.rowsAffected[0]) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
};
|
||||
}
|
||||
33
src/store/order/getById.js
Normal file
33
src/store/order/getById.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ORDER_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { loadDoc } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function getById(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a single order by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the order.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The order with its lines.
|
||||
* @throws {Error} If the order is not found.
|
||||
*
|
||||
* @example
|
||||
* const order = await store.order.getById(42, 5);
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const doc = await loadDoc(pool, null, { type: ORDER_TYPE, companyId, id });
|
||||
if (!doc) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { ORDER_TYPE } from '../../sql/manifestDoc.js';
|
||||
import createManifestDocStore from '../manifestDoc/createStore.js';
|
||||
|
||||
/**
|
||||
* Creates an order store (list, getById, create, update, delete) backed by the
|
||||
* manifest / manifestarticle tables.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {object} An object with `list`, `getById`, `create`, `update` and `delete` functions.
|
||||
*
|
||||
* @example
|
||||
* const store = createOrderStore(pool);
|
||||
* const orders = await store.list(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
export default function createOrderStore(pool) {
|
||||
return createManifestDocStore(pool, { type: ORDER_TYPE, label: 'Order' });
|
||||
}
|
||||
29
src/store/order/list.js
Normal file
29
src/store/order/list.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { LIST_MANIFESTS, ORDER_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { shapeSummary } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the orders of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, number: string, type: string, entityId: number|null, date: string}>>} The orders.
|
||||
*
|
||||
* @example
|
||||
* const orders = await store.order.list(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('companyId', companyId)
|
||||
.input('type', ORDER_TYPE)
|
||||
.query(LIST_MANIFESTS);
|
||||
|
||||
return result.recordset.map((row) => shapeSummary(ORDER_TYPE, row));
|
||||
};
|
||||
}
|
||||
132
src/store/order/update.js
Normal file
132
src/store/order/update.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
DELETE_LINES_BY_MANIFEST,
|
||||
GET_MANIFEST_HEADER,
|
||||
ORDER_TYPE,
|
||||
UPDATE_MANIFEST,
|
||||
} from '../../sql/manifestDoc.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Updates an order's header and/or lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the order to update.
|
||||
* @param {object} patch - The update patch.
|
||||
* @param {string} [patch.number] - The new order number.
|
||||
* @param {string} [patch.date] - The new order date.
|
||||
* @param {number} [patch.entityId] - The new customer entity id.
|
||||
* @param {Array<object>} [patch.lines] - The new order lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The updated order.
|
||||
* @throws {Error} If the order is not found.
|
||||
*
|
||||
* @example
|
||||
* const order = await store.order.update(42, 5, { number: 'B-2026-002', lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number');
|
||||
const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date');
|
||||
const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId');
|
||||
const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines');
|
||||
|
||||
if (!hasNumber && !hasDate && !hasEntityId && !hasLines) {
|
||||
throw new Error('Invalid order');
|
||||
}
|
||||
|
||||
let normalizedLines;
|
||||
if (hasLines) {
|
||||
normalizedLines = normalizeLines(patch.lines);
|
||||
}
|
||||
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const existing = await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', ORDER_TYPE)
|
||||
.query(GET_MANIFEST_HEADER);
|
||||
|
||||
const current = existing.recordset[0];
|
||||
if (!current) {
|
||||
throw new Error('Order not found');
|
||||
}
|
||||
|
||||
const summary = shapeSummary(ORDER_TYPE, current);
|
||||
|
||||
let number = summary.number;
|
||||
if (hasNumber) {
|
||||
number = patch.number?.trim?.() ? patch.number.trim() : null;
|
||||
if (!number) {
|
||||
throw new Error('Invalid order');
|
||||
}
|
||||
}
|
||||
|
||||
let date = summary.date;
|
||||
if (hasDate) {
|
||||
date = parseDate(patch.date);
|
||||
if (!date) {
|
||||
throw new Error('Invalid order');
|
||||
}
|
||||
}
|
||||
|
||||
let entityId = summary.entityId;
|
||||
if (hasEntityId) {
|
||||
entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
patch.entityId ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', ORDER_TYPE)
|
||||
.input('number', number)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(UPDATE_MANIFEST);
|
||||
|
||||
if (hasLines) {
|
||||
await request(pool, tx)
|
||||
.input('manifestId', id)
|
||||
.query(DELETE_LINES_BY_MANIFEST);
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId: id,
|
||||
rootCategoryId,
|
||||
lines: normalizedLines,
|
||||
ensureTax,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = await loadDoc(pool, tx, { type: ORDER_TYPE, companyId, id });
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import { GET_FULL_BY_HASH } from '../../sql/pic.js';
|
||||
import resolvePicAccess from './resolvePicAccess.js';
|
||||
|
||||
/**
|
||||
* Returns the full-size image (AVIF) for a given hash.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a full image by hash.
|
||||
*
|
||||
* @example
|
||||
* const fn = getImageByHash(pool);
|
||||
* const image = await fn(42, 'abc123');
|
||||
* // => { buffer, w, h, size, contentType: 'image/avif' }
|
||||
*/
|
||||
export default function getImageByHash(pool) {
|
||||
const authorize = resolvePicAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns the full-size image (AVIF) for a given hash.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} fullHash - The full image hash.
|
||||
* @returns {Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>} The full image.
|
||||
* @throws {Error} If the image is not found.
|
||||
*
|
||||
* @example
|
||||
* const image = await store.pic.getImageByHash(42, 'abc123');
|
||||
* // => { buffer, w, h, size, contentType: 'image/avif' }
|
||||
*/
|
||||
return async (googleAuthId, fullHash) => {
|
||||
await authorize(googleAuthId, fullHash);
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { GET_THUMB_BY_HASH } from '../../sql/pic.js';
|
||||
import resolvePicAccess from './resolvePicAccess.js';
|
||||
|
||||
/**
|
||||
* Returns the thumbnail image (AVIF) for a given hash.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that fetches a thumbnail by hash.
|
||||
*
|
||||
* @example
|
||||
* const fn = getThumbByHash(pool);
|
||||
* const thumb = await fn(42, 'abc123');
|
||||
* // => { buffer, w, h, contentType: 'image/avif' }
|
||||
*/
|
||||
export default function getThumbByHash(pool) {
|
||||
const authorize = resolvePicAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns the thumbnail image (AVIF) for a given hash.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} fullHash - The full image hash.
|
||||
* @returns {Promise<{buffer: Buffer, w: number, h: number, contentType: string}>} The thumbnail image.
|
||||
* @throws {Error} If the image is not found.
|
||||
*
|
||||
* @example
|
||||
* const thumb = await store.pic.getThumbByHash(42, 'abc123');
|
||||
* // => { buffer, w, h, contentType: 'image/avif' }
|
||||
*/
|
||||
return async (googleAuthId, fullHash) => {
|
||||
await authorize(googleAuthId, fullHash);
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_COMPANY_AND_ID } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a posterminal by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a posterminal by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the posterminal to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the posterminal is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.posterminal.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Posterminal not found');
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { POSTERMINAL_IDS_BY_COMPANY } from '../../sql/posterminal.js';
|
||||
|
||||
/**
|
||||
* Returns a map of category id to the posterminal ids assigned to it for a
|
||||
* company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that builds the category→posterminal map.
|
||||
*
|
||||
* @example
|
||||
* const fn = getCategoryPosterminalIds(pool);
|
||||
* const map = await fn(1);
|
||||
* // => { 5: [1, 2], 9: [1] }
|
||||
*/
|
||||
export default function getCategoryPosterminalIds(pool) {
|
||||
/**
|
||||
* Returns a map of category id to the posterminal ids assigned to it for a
|
||||
* company.
|
||||
*
|
||||
* @param {number} companyId - The id of the company.
|
||||
* @returns {Promise<Record<number, Array<number>>>} A map of category id to posterminal ids.
|
||||
*
|
||||
* @example
|
||||
* const map = await getCategoryPosterminalIds(pool)(1);
|
||||
* // => { 5: [1, 2], 9: [1] }
|
||||
*/
|
||||
return async (companyId) => {
|
||||
const result = await pool
|
||||
.request()
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { LIST_BY_COMPANY } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Lists the posterminals of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists posterminals.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const terminals = await fn(42);
|
||||
* // => [{ id, name, token, pairingpin }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the posterminals of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, name: string, token: string, pairingpin: string}>>} The posterminals.
|
||||
*
|
||||
* @example
|
||||
* const terminals = await store.posterminal.list(42);
|
||||
* // => [{ id, name, token, pairingpin }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { GET_BY_ID, SET_OFF, LIST_CATEGORY_IDS } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Removes a category assignment from a posterminal (turns it off).
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that removes a category from a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = setOff(pool);
|
||||
* const result = await fn(42, 5, 9);
|
||||
* // => { id, categoryIds: [...] }
|
||||
*/
|
||||
export default function setOff(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Removes a category assignment from a posterminal (turns it off).
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} posterminalId - The id of the posterminal.
|
||||
* @param {number} categoryId - The id of the category to remove.
|
||||
* @returns {Promise<{id: number, categoryIds: Array<number>}>} The posterminal with its remaining category ids.
|
||||
* @throws {Error} If the posterminal is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.posterminal.setOff(42, 5, 9);
|
||||
* // => { id, categoryIds: [...] }
|
||||
*/
|
||||
return async (googleAuthId, posterminalId, categoryId) => {
|
||||
if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) {
|
||||
throw new Error('Invalid posterminal or category');
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { GET_BY_ID, SET_OFF_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Removes all category assignments from a posterminal.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that clears all categories from a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = setOffAll(pool);
|
||||
* const result = await fn(42, 5);
|
||||
* // => { id, categoryIds: [] }
|
||||
*/
|
||||
export default function setOffAll(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Removes all category assignments from a posterminal.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} posterminalId - The id of the posterminal.
|
||||
* @returns {Promise<{id: number, categoryIds: Array<number>}>} The posterminal with an empty category list.
|
||||
* @throws {Error} If the posterminal is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.posterminal.setOffAll(42, 5);
|
||||
* // => { id, categoryIds: [] }
|
||||
*/
|
||||
return async (googleAuthId, posterminalId) => {
|
||||
if (!Number.isFinite(posterminalId)) {
|
||||
throw new Error('Invalid posterminal');
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { GET_BY_ID, SET_ON, LIST_CATEGORY_IDS } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Assigns a category to a posterminal (turns it on for that category).
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that assigns a category to a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = setOn(pool);
|
||||
* const result = await fn(42, 5, 9);
|
||||
* // => { id, categoryIds: [9, ...] }
|
||||
*/
|
||||
export default function setOn(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Assigns a category to a posterminal (turns it on for that category).
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} posterminalId - The id of the posterminal.
|
||||
* @param {number} categoryId - The id of the category to assign.
|
||||
* @returns {Promise<{id: number, categoryIds: Array<number>}>} The posterminal with its category ids.
|
||||
* @throws {Error} If the posterminal is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.posterminal.setOn(42, 5, 9);
|
||||
* // => { id, categoryIds: [9, ...] }
|
||||
*/
|
||||
return async (googleAuthId, posterminalId, categoryId) => {
|
||||
if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) {
|
||||
throw new Error('Invalid posterminal or category');
|
||||
|
||||
@@ -2,20 +2,21 @@ import { GET_BY_ID, SET_ON_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { GET_COMPANY_ROOT_CATEGORY } from '../../sql/company.js';
|
||||
|
||||
/**
|
||||
* Assigns all categories of a company to a posterminal.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that assigns all categories to a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = setOnAll(pool);
|
||||
* const result = await fn(42, 5);
|
||||
* // => { id, categoryIds: [...] }
|
||||
*/
|
||||
export default function setOnAll(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Assigns all categories of the user's selected company to a posterminal.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} posterminalId - The id of the posterminal.
|
||||
* @returns {Promise<{id: number, categoryIds: Array<number>}>} The posterminal with all category ids.
|
||||
* @throws {Error} If the posterminal is not found.
|
||||
*
|
||||
* @example
|
||||
* const result = await store.posterminal.setOnAll(42, 5);
|
||||
* // => { id, categoryIds: [...] }
|
||||
*/
|
||||
return async (googleAuthId, posterminalId) => {
|
||||
if (!Number.isFinite(posterminalId)) {
|
||||
throw new Error('Invalid posterminal');
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { GET_BY_ID, CREATE, UPDATE } from '../../sql/posterminal.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Creates or updates a posterminal for a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that upserts a posterminal.
|
||||
*
|
||||
* @example
|
||||
* const fn = upsert(pool);
|
||||
* const terminal = await fn(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' });
|
||||
* // => { id, name, token, pairingpin }
|
||||
*/
|
||||
export default function upsert(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates or updates a posterminal for the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The posterminal input.
|
||||
* @param {string} input.name - The posterminal name.
|
||||
* @param {string} input.pairingpin - The pairing pin.
|
||||
* @param {string} [input.token] - Optional token.
|
||||
* @param {number} [input.id] - Optional id to update an existing posterminal.
|
||||
* @returns {Promise<{id: number, name: string, token: string, pairingpin: string}>} The upserted posterminal.
|
||||
* @throws {Error} If the posterminal is invalid.
|
||||
*
|
||||
* @example
|
||||
* const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' });
|
||||
* // => { id, name, token, pairingpin }
|
||||
*/
|
||||
return async (googleAuthId, input = {}) => {
|
||||
const name = input.name?.trim();
|
||||
if (!name) {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { buildPurgeSql } from '../sql/purge.js';
|
||||
|
||||
/**
|
||||
* Purges (wipes) all data from the database.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that purges the database.
|
||||
*
|
||||
* @example
|
||||
* const fn = purge(pool);
|
||||
* await fn();
|
||||
*/
|
||||
export default function purge(pool) {
|
||||
/**
|
||||
* Purges (wipes) all data from the database.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @example
|
||||
* await store.purge();
|
||||
*/
|
||||
return async () => {
|
||||
await pool.request().query(buildPurgeSql());
|
||||
};
|
||||
|
||||
82
src/store/quote/create.js
Normal file
82
src/store/quote/create.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { CREATE_MANIFEST, QUOTE_TYPE } from '../../sql/manifestDoc.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Creates a new quote for the user's selected company with its lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} body - The quote input.
|
||||
* @param {string} body.number - The quote number.
|
||||
* @param {string} body.date - The quote date.
|
||||
* @param {number} [body.entityId] - The id of the customer entity.
|
||||
* @param {Array<object>} [body.lines] - The quote lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The created quote.
|
||||
* @throws {Error} If the quote is invalid.
|
||||
*
|
||||
* @example
|
||||
* const quote = await store.quote.create(42, { number: 'A-2026-001', date: '2026-09-01', entityId: 12, lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, body = {}) => {
|
||||
const number = body.number?.trim?.() ? body.number.trim() : null;
|
||||
const date = parseDate(body.date);
|
||||
if (!number || !date) {
|
||||
throw new Error('Invalid quote');
|
||||
}
|
||||
|
||||
const lines = normalizeLines(body.lines ?? []);
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
body.entityId ?? null,
|
||||
);
|
||||
|
||||
const created = await request(pool, tx)
|
||||
.input('companyId', companyId)
|
||||
.input('number', number)
|
||||
.input('type', QUOTE_TYPE)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(CREATE_MANIFEST);
|
||||
|
||||
const manifestId = created.recordset[0].id;
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId,
|
||||
rootCategoryId,
|
||||
lines,
|
||||
ensureTax,
|
||||
});
|
||||
|
||||
const doc = await loadDoc(pool, tx, {
|
||||
type: QUOTE_TYPE,
|
||||
companyId,
|
||||
id: manifestId,
|
||||
});
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
36
src/store/quote/delete.js
Normal file
36
src/store/quote/delete.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { DELETE_MANIFEST, QUOTE_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a quote by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the quote to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the quote is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.quote.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', QUOTE_TYPE)
|
||||
.query(DELETE_MANIFEST);
|
||||
|
||||
if (!result.rowsAffected[0]) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
};
|
||||
}
|
||||
33
src/store/quote/getById.js
Normal file
33
src/store/quote/getById.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { QUOTE_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { loadDoc } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function getById(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Returns a single quote by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the quote.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The quote with its lines.
|
||||
* @throws {Error} If the quote is not found.
|
||||
*
|
||||
* @example
|
||||
* const quote = await store.quote.getById(42, 5);
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const doc = await loadDoc(pool, null, { type: QUOTE_TYPE, companyId, id });
|
||||
if (!doc) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { QUOTE_TYPE } from '../../sql/manifestDoc.js';
|
||||
import createManifestDocStore from '../manifestDoc/createStore.js';
|
||||
|
||||
/**
|
||||
* Creates a quote store (list, getById, create, update, delete) backed by the
|
||||
* manifest / manifestarticle tables.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {object} An object with `list`, `getById`, `create`, `update` and `delete` functions.
|
||||
*
|
||||
* @example
|
||||
* const store = createQuoteStore(pool);
|
||||
* const quotes = await store.list(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
export default function createQuoteStore(pool) {
|
||||
return createManifestDocStore(pool, { type: QUOTE_TYPE, label: 'Quote' });
|
||||
}
|
||||
29
src/store/quote/list.js
Normal file
29
src/store/quote/list.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { LIST_MANIFESTS, QUOTE_TYPE } from '../../sql/manifestDoc.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import { shapeSummary } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the quotes of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, number: string, type: string, entityId: number|null, date: string}>>} The quotes.
|
||||
*
|
||||
* @example
|
||||
* const quotes = await store.quote.list(42);
|
||||
* // => [{ id, number, type, entityId, date }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('companyId', companyId)
|
||||
.input('type', QUOTE_TYPE)
|
||||
.query(LIST_MANIFESTS);
|
||||
|
||||
return result.recordset.map((row) => shapeSummary(QUOTE_TYPE, row));
|
||||
};
|
||||
}
|
||||
132
src/store/quote/update.js
Normal file
132
src/store/quote/update.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
DELETE_LINES_BY_MANIFEST,
|
||||
GET_MANIFEST_HEADER,
|
||||
QUOTE_TYPE,
|
||||
UPDATE_MANIFEST,
|
||||
} from '../../sql/manifestDoc.js';
|
||||
import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import taxEnsure from '../tax/ensure.js';
|
||||
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
|
||||
import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js';
|
||||
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
const ensureTax = taxEnsure(pool);
|
||||
|
||||
/**
|
||||
* Updates a quote's header and/or lines.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the quote to update.
|
||||
* @param {object} patch - The update patch.
|
||||
* @param {string} [patch.number] - The new quote number.
|
||||
* @param {string} [patch.date] - The new quote date.
|
||||
* @param {number} [patch.entityId] - The new customer entity id.
|
||||
* @param {Array<object>} [patch.lines] - The new quote lines.
|
||||
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array<object>}>} The updated quote.
|
||||
* @throws {Error} If the quote is not found.
|
||||
*
|
||||
* @example
|
||||
* const quote = await store.quote.update(42, 5, { number: 'A-2026-002', lines: [...] });
|
||||
* // => { id, number, type, entityId, date, lines: [...] }
|
||||
*/
|
||||
return async (googleAuthId, id, patch = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
|
||||
const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number');
|
||||
const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date');
|
||||
const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId');
|
||||
const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines');
|
||||
|
||||
if (!hasNumber && !hasDate && !hasEntityId && !hasLines) {
|
||||
throw new Error('Invalid quote');
|
||||
}
|
||||
|
||||
let normalizedLines;
|
||||
if (hasLines) {
|
||||
normalizedLines = normalizeLines(patch.lines);
|
||||
}
|
||||
|
||||
const { companyId, rootCategoryId } = await access(googleAuthId);
|
||||
|
||||
const tx = pool.transaction();
|
||||
await tx.begin();
|
||||
|
||||
try {
|
||||
const existing = await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', QUOTE_TYPE)
|
||||
.query(GET_MANIFEST_HEADER);
|
||||
|
||||
const current = existing.recordset[0];
|
||||
if (!current) {
|
||||
throw new Error('Quote not found');
|
||||
}
|
||||
|
||||
const summary = shapeSummary(QUOTE_TYPE, current);
|
||||
|
||||
let number = summary.number;
|
||||
if (hasNumber) {
|
||||
number = patch.number?.trim?.() ? patch.number.trim() : null;
|
||||
if (!number) {
|
||||
throw new Error('Invalid quote');
|
||||
}
|
||||
}
|
||||
|
||||
let date = summary.date;
|
||||
if (hasDate) {
|
||||
date = parseDate(patch.date);
|
||||
if (!date) {
|
||||
throw new Error('Invalid quote');
|
||||
}
|
||||
}
|
||||
|
||||
let entityId = summary.entityId;
|
||||
if (hasEntityId) {
|
||||
entityId = await assertCustomerEntity(
|
||||
pool,
|
||||
tx,
|
||||
companyId,
|
||||
patch.entityId ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
await request(pool, tx)
|
||||
.input('id', id)
|
||||
.input('companyId', companyId)
|
||||
.input('type', QUOTE_TYPE)
|
||||
.input('number', number)
|
||||
.input('entityId', entityId)
|
||||
.input('date', date)
|
||||
.query(UPDATE_MANIFEST);
|
||||
|
||||
if (hasLines) {
|
||||
await request(pool, tx)
|
||||
.input('manifestId', id)
|
||||
.query(DELETE_LINES_BY_MANIFEST);
|
||||
|
||||
await insertLines(pool, tx, {
|
||||
manifestId: id,
|
||||
rootCategoryId,
|
||||
lines: normalizedLines,
|
||||
ensureTax,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = await loadDoc(pool, tx, { type: QUOTE_TYPE, companyId, id });
|
||||
await tx.commit();
|
||||
return doc;
|
||||
} catch (err) {
|
||||
try {
|
||||
await tx.rollback();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -4,20 +4,22 @@ import request from '../request.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import ensureLieferantenGroup from './ensureLieferantenGroup.js';
|
||||
|
||||
/**
|
||||
* Creates a new supplier for a company, recording history.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that creates a supplier.
|
||||
*
|
||||
* @example
|
||||
* const fn = create(pool);
|
||||
* const supplier = await fn(42, { name: 'Acme Supplies' });
|
||||
* // => { id, name, createdAt, updatedAt }
|
||||
*/
|
||||
export default function create(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Creates a new supplier for the user's selected company, recording history.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {object} input - The supplier input.
|
||||
* @param {string} input.name - The supplier name.
|
||||
* @returns {Promise<{id: number, name: string, createdAt: string, updatedAt: string}>} The created supplier.
|
||||
* @throws {Error} If the name is empty.
|
||||
*
|
||||
* @example
|
||||
* const supplier = await store.supplier.create(42, { name: 'Acme Supplies' });
|
||||
* // => { id, name, createdAt, updatedAt }
|
||||
*/
|
||||
return async (googleAuthId, { name } = {}) => {
|
||||
const cleaned = name?.trim();
|
||||
if (!cleaned) {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { DELETE_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Deletes a supplier by its id within a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that deletes a supplier.
|
||||
*
|
||||
* @example
|
||||
* const fn = del(pool);
|
||||
* await fn(42, 5);
|
||||
*/
|
||||
export default function del(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Deletes a supplier by its id within the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the supplier to delete.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the supplier is not found.
|
||||
*
|
||||
* @example
|
||||
* await store.supplier.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Supplier not found');
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { LIST_SUPPLIERS_BY_COMPANY, SUPPLIER_GROUP_NAME } from '../../sql/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
|
||||
/**
|
||||
* Lists the suppliers of a company.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that lists suppliers.
|
||||
*
|
||||
* @example
|
||||
* const fn = list(pool);
|
||||
* const suppliers = await fn(42);
|
||||
* // => [{ id, name, createdAt, updatedAt }]
|
||||
*/
|
||||
export default function list(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Lists the suppliers of the user's selected company.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @returns {Promise<Array<{id: number, name: string, createdAt: string, updatedAt: string}>>} The suppliers.
|
||||
*
|
||||
* @example
|
||||
* const suppliers = await store.supplier.list(42);
|
||||
* // => [{ id, name, createdAt, updatedAt }]
|
||||
*/
|
||||
return async (googleAuthId) => {
|
||||
const { companyId } = await access(googleAuthId);
|
||||
|
||||
|
||||
@@ -17,20 +17,23 @@ function shape(row) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the name of a supplier, recording history.
|
||||
*
|
||||
* @param {object} pool - The mssql connection pool.
|
||||
* @returns {Function} An async function that updates a supplier.
|
||||
*
|
||||
* @example
|
||||
* const fn = update(pool);
|
||||
* const supplier = await fn(42, 5, { name: 'Acme GmbH' });
|
||||
* // => { id, name, createdAt, updatedAt }
|
||||
*/
|
||||
export default function update(pool) {
|
||||
const access = resolveSelectedCompanyAccess(pool);
|
||||
|
||||
/**
|
||||
* Updates the name of a supplier, recording history.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {number} id - The id of the supplier to update.
|
||||
* @param {object} input - The update input.
|
||||
* @param {string} input.name - The new supplier name.
|
||||
* @returns {Promise<{id: number, name: string, createdAt: string, updatedAt: string}>} The updated supplier.
|
||||
* @throws {Error} If the supplier is not found or the name is empty.
|
||||
*
|
||||
* @example
|
||||
* const supplier = await store.supplier.update(42, 5, { name: 'Acme GmbH' });
|
||||
* // => { id, name, createdAt, updatedAt }
|
||||
*/
|
||||
return async (googleAuthId, id, { name } = {}) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
throw new Error('Supplier not found');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user