rerobe-js-orm 4.8.8 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/constants/marketplace-constants.d.ts +52 -0
- package/lib/constants/marketplace-constants.js +82 -0
- package/lib/constants/product-constants.d.ts +6 -0
- package/lib/constants/product-constants.js +43 -1
- package/lib/constants/tax-constants.d.ts +19 -0
- package/lib/constants/tax-constants.js +70 -0
- package/lib/factories/Product/RetailProductFactory.d.ts +102 -0
- package/lib/factories/Product/RetailProductFactory.js +260 -0
- package/lib/form-states/Product/ProductFormState.d.ts +9 -0
- package/lib/form-states/Product/ProductFormState.js +46 -0
- package/lib/helpers/MarketplaceProductHelpers.d.ts +181 -0
- package/lib/helpers/MarketplaceProductHelpers.js +555 -0
- package/lib/helpers/OrderHelpers.d.ts +5 -2
- package/lib/helpers/OrderHelpers.js +18 -4
- package/lib/helpers/PricingHelpers.d.ts +29 -0
- package/lib/helpers/PricingHelpers.js +215 -0
- package/lib/helpers/ProductInventoryHelpers.d.ts +31 -0
- package/lib/helpers/ProductInventoryHelpers.js +66 -0
- package/lib/helpers/ProductSkuHelpers.d.ts +13 -0
- package/lib/helpers/ProductSkuHelpers.js +70 -0
- package/lib/helpers/TaxHelpers.d.ts +15 -0
- package/lib/helpers/TaxHelpers.js +76 -0
- package/lib/helpers/marketplace/CommissionPolicy.d.ts +20 -0
- package/lib/helpers/marketplace/CommissionPolicy.js +9 -0
- package/lib/helpers/marketplace/CommissionPolicyRegistry.d.ts +4 -0
- package/lib/helpers/marketplace/CommissionPolicyRegistry.js +21 -0
- package/lib/helpers/marketplace/EvenPerSellerStripeFeePolicy.d.ts +5 -0
- package/lib/helpers/marketplace/EvenPerSellerStripeFeePolicy.js +46 -0
- package/lib/helpers/marketplace/FeeAllocationPolicy.d.ts +10 -0
- package/lib/helpers/marketplace/FeeAllocationPolicy.js +36 -0
- package/lib/helpers/marketplace/FeeAllocationPolicyRegistry.d.ts +6 -0
- package/lib/helpers/marketplace/FeeAllocationPolicyRegistry.js +26 -0
- package/lib/helpers/marketplace/InventoryPolicy.d.ts +18 -0
- package/lib/helpers/marketplace/InventoryPolicy.js +7 -0
- package/lib/helpers/marketplace/InventoryPolicyRegistry.d.ts +4 -0
- package/lib/helpers/marketplace/InventoryPolicyRegistry.js +29 -0
- package/lib/helpers/marketplace/LineNetCommissionPolicy.d.ts +5 -0
- package/lib/helpers/marketplace/LineNetCommissionPolicy.js +25 -0
- package/lib/helpers/marketplace/MarketplaceErrors.d.ts +6 -0
- package/lib/helpers/marketplace/MarketplaceErrors.js +21 -0
- package/lib/helpers/marketplace/MarketplaceLedgerHelpers.d.ts +40 -0
- package/lib/helpers/marketplace/MarketplaceLedgerHelpers.js +120 -0
- package/lib/helpers/marketplace/MarketplaceLegacyAdapters.d.ts +53 -0
- package/lib/helpers/marketplace/MarketplaceLegacyAdapters.js +99 -0
- package/lib/helpers/marketplace/MarketplaceLineDisplayHelpers.d.ts +40 -0
- package/lib/helpers/marketplace/MarketplaceLineDisplayHelpers.js +125 -0
- package/lib/helpers/marketplace/MarketplaceOrderHelpers.d.ts +15 -0
- package/lib/helpers/marketplace/MarketplaceOrderHelpers.js +77 -0
- package/lib/helpers/marketplace/MultivariantLocationInventoryPolicy.d.ts +9 -0
- package/lib/helpers/marketplace/MultivariantLocationInventoryPolicy.js +60 -0
- package/lib/helpers/marketplace/OneOfAKindInventoryPolicy.d.ts +9 -0
- package/lib/helpers/marketplace/OneOfAKindInventoryPolicy.js +55 -0
- package/lib/helpers/marketplace/OriginalCommissionShareCommissionPolicy.d.ts +5 -0
- package/lib/helpers/marketplace/OriginalCommissionShareCommissionPolicy.js +26 -0
- package/lib/helpers/marketplace/ProportionalToLineNetPolicy.d.ts +5 -0
- package/lib/helpers/marketplace/ProportionalToLineNetPolicy.js +35 -0
- package/lib/helpers/marketplace/UntrackedStockInventoryPolicy.d.ts +7 -0
- package/lib/helpers/marketplace/UntrackedStockInventoryPolicy.js +23 -0
- package/lib/index.d.ts +18 -1
- package/lib/index.js +56 -1
- package/lib/models/Product.d.ts +16 -0
- package/lib/models/Product.js +140 -0
- package/lib/types/pricing-types.d.ts +85 -0
- package/lib/types/pricing-types.js +5 -0
- package/lib/types/rerobe-order-types.d.ts +86 -0
- package/lib/types/rerobe-product-types.d.ts +19 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IsTaxableValue, OrderLinePricing, OrderLinePricingLike, ProductPricing, ProductPricingLike, RateInput, TaxResolution, VariantPricing, VariantPricingLike } from '../types/pricing-types';
|
|
2
|
+
export default class PricingHelpers {
|
|
3
|
+
static isTaxableYes(value: IsTaxableValue): boolean;
|
|
4
|
+
static resolveTaxRate(input: RateInput): TaxResolution;
|
|
5
|
+
static passThroughString(value: string | number | undefined | null): string;
|
|
6
|
+
static toNetString(gross: string | number | undefined | null, rate: number): string;
|
|
7
|
+
static toGrossString(net: string | number | undefined | null, rate: number): string;
|
|
8
|
+
static getProductPricing(product: ProductPricingLike | null | undefined, opts: {
|
|
9
|
+
countryCode: string;
|
|
10
|
+
}): ProductPricing;
|
|
11
|
+
static getVariantPricing(variant: VariantPricingLike | null | undefined, parent: ProductPricingLike | null | undefined, opts: {
|
|
12
|
+
countryCode: string;
|
|
13
|
+
}): VariantPricing;
|
|
14
|
+
static getOrderLinePricing(line: OrderLinePricingLike | null | undefined): OrderLinePricing;
|
|
15
|
+
static buildAccountingSavePatch(values: {
|
|
16
|
+
price?: number | string;
|
|
17
|
+
isTaxable?: IsTaxableValue;
|
|
18
|
+
taxCategory?: string | null;
|
|
19
|
+
}, countryCode: string): {
|
|
20
|
+
price: string;
|
|
21
|
+
taxCategory: string;
|
|
22
|
+
isTaxable: 'yes' | 'no';
|
|
23
|
+
};
|
|
24
|
+
static buildAccountingEditSeed(values: {
|
|
25
|
+
price?: number | string;
|
|
26
|
+
isTaxable?: IsTaxableValue;
|
|
27
|
+
taxCategory?: string | null;
|
|
28
|
+
}, countryCode: string): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tax_constants_1 = require("../constants/tax-constants");
|
|
4
|
+
const TaxHelpers_1 = require("./TaxHelpers");
|
|
5
|
+
// Catalog vs frozen-rate type narrow used by `resolveTaxRate`. We treat
|
|
6
|
+
// any object that surfaces `countryCode` or `taxCategory` as catalog mode
|
|
7
|
+
// and everything else as frozen-rate mode (which only carries `taxRate`
|
|
8
|
+
// and `isTaxable`).
|
|
9
|
+
function isCatalogInput(input) {
|
|
10
|
+
return input.countryCode !== undefined || input.taxCategory !== undefined;
|
|
11
|
+
}
|
|
12
|
+
function coerceFrozenRate(rate) {
|
|
13
|
+
if (rate === null || rate === undefined)
|
|
14
|
+
return 0;
|
|
15
|
+
const n = typeof rate === 'number' ? rate : parseFloat(String(rate));
|
|
16
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
17
|
+
}
|
|
18
|
+
function readMoneyAmount(value) {
|
|
19
|
+
if (value === null || value === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
if (typeof value === 'number' || typeof value === 'string')
|
|
22
|
+
return value;
|
|
23
|
+
return value.amount === undefined ? null : value.amount;
|
|
24
|
+
}
|
|
25
|
+
// Pricing helpers — pure functions that resolve effective tax rates and
|
|
26
|
+
// project net/gross snapshots for products, variants, and order lines.
|
|
27
|
+
//
|
|
28
|
+
// Two lookup modes:
|
|
29
|
+
// - Catalog (country + tax category) → product editors, list screens
|
|
30
|
+
// - Frozen rate (taxRate captured at order time) → orders, refunds, POS
|
|
31
|
+
//
|
|
32
|
+
// Lifted from the ribbn-web-app `lib/pricing/core` module so backend,
|
|
33
|
+
// mobile, and web all share the same conversions and snapshot shapes.
|
|
34
|
+
class PricingHelpers {
|
|
35
|
+
static isTaxableYes(value) {
|
|
36
|
+
if (value === true)
|
|
37
|
+
return true;
|
|
38
|
+
if (typeof value === 'string')
|
|
39
|
+
return value.toLowerCase() === 'yes';
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
static resolveTaxRate(input) {
|
|
43
|
+
const taxable = PricingHelpers.isTaxableYes(input.isTaxable);
|
|
44
|
+
if (!taxable) {
|
|
45
|
+
return { rate: 0, taxable: false };
|
|
46
|
+
}
|
|
47
|
+
if (isCatalogInput(input)) {
|
|
48
|
+
const category = TaxHelpers_1.default.normalizeTaxCategory(input.taxCategory);
|
|
49
|
+
if (category === tax_constants_1.TAX_CATEGORIES.EXEMPT) {
|
|
50
|
+
return { rate: 0, taxable: true, category };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
rate: TaxHelpers_1.default.getTaxRate(input.countryCode, category),
|
|
54
|
+
taxable: true,
|
|
55
|
+
category,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const frozen = input;
|
|
59
|
+
return { rate: coerceFrozenRate(frozen.taxRate), taxable: true };
|
|
60
|
+
}
|
|
61
|
+
// Pass-through string normalization used when no rate applies (exempt
|
|
62
|
+
// / non-taxable). Trims and returns the stored value as a plain string
|
|
63
|
+
// so callers don't have to special-case empty inputs.
|
|
64
|
+
static passThroughString(value) {
|
|
65
|
+
if (value === null || value === undefined)
|
|
66
|
+
return '';
|
|
67
|
+
if (typeof value === 'number')
|
|
68
|
+
return String(value);
|
|
69
|
+
return String(value).trim();
|
|
70
|
+
}
|
|
71
|
+
static toNetString(gross, rate) {
|
|
72
|
+
return TaxHelpers_1.default.grossToNetPriceString(gross, rate);
|
|
73
|
+
}
|
|
74
|
+
static toGrossString(net, rate) {
|
|
75
|
+
return TaxHelpers_1.default.netToGrossPriceString(net, rate);
|
|
76
|
+
}
|
|
77
|
+
// Pricing snapshot for a product (parent fields). Resolves the rate
|
|
78
|
+
// once, then projects every price-like field into both stored (net)
|
|
79
|
+
// and displayable (gross) forms.
|
|
80
|
+
static getProductPricing(product, opts) {
|
|
81
|
+
const safeProduct = product || {};
|
|
82
|
+
const { rate, taxable } = PricingHelpers.resolveTaxRate({
|
|
83
|
+
countryCode: opts.countryCode,
|
|
84
|
+
taxCategory: safeProduct.taxCategory,
|
|
85
|
+
isTaxable: safeProduct.isTaxable,
|
|
86
|
+
});
|
|
87
|
+
const projectGross = (net) => {
|
|
88
|
+
const trimmed = PricingHelpers.passThroughString(net);
|
|
89
|
+
if (!trimmed)
|
|
90
|
+
return '';
|
|
91
|
+
if (!rate)
|
|
92
|
+
return trimmed;
|
|
93
|
+
return PricingHelpers.toGrossString(trimmed, rate);
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
rate,
|
|
97
|
+
taxable,
|
|
98
|
+
priceNet: PricingHelpers.passThroughString(safeProduct.price),
|
|
99
|
+
salePriceNet: PricingHelpers.passThroughString(safeProduct.salePrice),
|
|
100
|
+
compareAtPriceNet: PricingHelpers.passThroughString(safeProduct.compareAtPrice),
|
|
101
|
+
costPerItemNet: PricingHelpers.passThroughString(safeProduct.costPerItem),
|
|
102
|
+
priceGross: projectGross(safeProduct.price),
|
|
103
|
+
salePriceGross: projectGross(safeProduct.salePrice),
|
|
104
|
+
compareAtPriceGross: projectGross(safeProduct.compareAtPrice),
|
|
105
|
+
costPerItemGross: projectGross(safeProduct.costPerItem),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Pricing snapshot for a single variant inventory row. Honors the
|
|
109
|
+
// variant-level `taxable` override; falls back to the parent product's
|
|
110
|
+
// `isTaxable` and `taxCategory` when the variant doesn't specify one.
|
|
111
|
+
static getVariantPricing(variant, parent, opts) {
|
|
112
|
+
const safeVariant = variant || {};
|
|
113
|
+
const safeParent = parent || {};
|
|
114
|
+
const variantTaxableSpecified = safeVariant.taxable !== undefined && safeVariant.taxable !== null;
|
|
115
|
+
const effectiveTaxable = variantTaxableSpecified
|
|
116
|
+
? PricingHelpers.isTaxableYes(safeVariant.taxable)
|
|
117
|
+
? 'yes'
|
|
118
|
+
: 'no'
|
|
119
|
+
: safeParent.isTaxable;
|
|
120
|
+
const { rate, taxable } = PricingHelpers.resolveTaxRate({
|
|
121
|
+
countryCode: opts.countryCode,
|
|
122
|
+
taxCategory: safeParent.taxCategory,
|
|
123
|
+
isTaxable: effectiveTaxable,
|
|
124
|
+
});
|
|
125
|
+
const projectGross = (net) => {
|
|
126
|
+
const trimmed = PricingHelpers.passThroughString(net);
|
|
127
|
+
if (!trimmed)
|
|
128
|
+
return '';
|
|
129
|
+
if (!rate)
|
|
130
|
+
return trimmed;
|
|
131
|
+
return PricingHelpers.toGrossString(trimmed, rate);
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
rate,
|
|
135
|
+
taxable,
|
|
136
|
+
priceNet: PricingHelpers.passThroughString(safeVariant.price),
|
|
137
|
+
salePriceNet: PricingHelpers.passThroughString(safeVariant.salePrice),
|
|
138
|
+
compareAtPriceNet: PricingHelpers.passThroughString(safeVariant.compareAtPrice),
|
|
139
|
+
costPerItemNet: PricingHelpers.passThroughString(safeVariant.costPerItem),
|
|
140
|
+
priceGross: projectGross(safeVariant.price),
|
|
141
|
+
salePriceGross: projectGross(safeVariant.salePrice),
|
|
142
|
+
compareAtPriceGross: projectGross(safeVariant.compareAtPrice),
|
|
143
|
+
costPerItemGross: projectGross(safeVariant.costPerItem),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Pricing snapshot for an order/refund/POS line item. Uses the line's
|
|
147
|
+
// frozen `taxRate` field captured at order creation rather than
|
|
148
|
+
// consulting the catalog — at order time we want to display the tax
|
|
149
|
+
// that was actually charged, not whatever the current catalog says.
|
|
150
|
+
static getOrderLinePricing(line) {
|
|
151
|
+
const safeLine = line || {};
|
|
152
|
+
const { rate, taxable } = PricingHelpers.resolveTaxRate({
|
|
153
|
+
taxRate: safeLine.taxRate,
|
|
154
|
+
isTaxable: safeLine.isTaxable,
|
|
155
|
+
});
|
|
156
|
+
const projectGross = (net) => {
|
|
157
|
+
const trimmed = PricingHelpers.passThroughString(net);
|
|
158
|
+
if (!trimmed)
|
|
159
|
+
return '';
|
|
160
|
+
if (!rate)
|
|
161
|
+
return trimmed;
|
|
162
|
+
return PricingHelpers.toGrossString(trimmed, rate);
|
|
163
|
+
};
|
|
164
|
+
const unit = PricingHelpers.passThroughString(readMoneyAmount(safeLine.originalUnitPrice));
|
|
165
|
+
const total = PricingHelpers.passThroughString(readMoneyAmount(safeLine.originalTotalPrice));
|
|
166
|
+
const sale = PricingHelpers.passThroughString(readMoneyAmount(safeLine.salePrice));
|
|
167
|
+
return {
|
|
168
|
+
rate,
|
|
169
|
+
taxable,
|
|
170
|
+
unitPriceNet: unit,
|
|
171
|
+
unitPriceGross: projectGross(unit),
|
|
172
|
+
totalPriceNet: total,
|
|
173
|
+
totalPriceGross: projectGross(total),
|
|
174
|
+
salePriceNet: sale,
|
|
175
|
+
salePriceGross: projectGross(sale),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Convenience helper for the retail accounting "save" path:
|
|
179
|
+
// - merchant types gross
|
|
180
|
+
// - we resolve the catalog rate
|
|
181
|
+
// - return the triple to persist (net price + normalized category +
|
|
182
|
+
// normalized isTaxable)
|
|
183
|
+
//
|
|
184
|
+
// Lifted from ribbn-web-app `ProductDetailsV2.buildAccountingSavePatch`.
|
|
185
|
+
static buildAccountingSavePatch(values, countryCode) {
|
|
186
|
+
const isTaxable = PricingHelpers.isTaxableYes(values.isTaxable);
|
|
187
|
+
const taxCategory = isTaxable ? TaxHelpers_1.default.normalizeTaxCategory(values.taxCategory) : tax_constants_1.TAX_CATEGORIES.EXEMPT;
|
|
188
|
+
const { rate } = PricingHelpers.resolveTaxRate({
|
|
189
|
+
countryCode,
|
|
190
|
+
taxCategory,
|
|
191
|
+
isTaxable: isTaxable ? 'yes' : 'no',
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
price: PricingHelpers.toNetString(values.price, rate),
|
|
195
|
+
taxCategory: String(taxCategory),
|
|
196
|
+
isTaxable: isTaxable ? 'yes' : 'no',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Convenience helper for the retail accounting "edit" path: seeds the
|
|
200
|
+
// price input with the gross-converted value of the stored net price
|
|
201
|
+
// so the merchant always sees / edits gross.
|
|
202
|
+
static buildAccountingEditSeed(values, countryCode) {
|
|
203
|
+
const taxCategory = TaxHelpers_1.default.normalizeTaxCategory(values.taxCategory);
|
|
204
|
+
const isTaxable = PricingHelpers.isTaxableYes(values.isTaxable);
|
|
205
|
+
const { rate } = PricingHelpers.resolveTaxRate({
|
|
206
|
+
countryCode,
|
|
207
|
+
taxCategory,
|
|
208
|
+
isTaxable: isTaxable ? 'yes' : 'no',
|
|
209
|
+
});
|
|
210
|
+
if (!rate)
|
|
211
|
+
return String(values.price === undefined || values.price === null ? '' : values.price);
|
|
212
|
+
return PricingHelpers.toGrossString(values.price, rate);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
exports.default = PricingHelpers;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const PLACEHOLDER_VARIANT_ID = "default";
|
|
2
|
+
export interface PlaceholderVariantLocation {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
incomingAmount: number;
|
|
6
|
+
availableAmount: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PlaceholderVariantInventory {
|
|
9
|
+
id: string;
|
|
10
|
+
options: {
|
|
11
|
+
[k: string]: never;
|
|
12
|
+
};
|
|
13
|
+
price?: string | null;
|
|
14
|
+
sku?: string | null;
|
|
15
|
+
taxable?: 'yes' | 'no' | null;
|
|
16
|
+
continueSellingWhenOutOfStock?: boolean | null;
|
|
17
|
+
locations?: PlaceholderVariantLocation[] | null;
|
|
18
|
+
}
|
|
19
|
+
export interface PlaceholderVariantInput {
|
|
20
|
+
sku?: string | null;
|
|
21
|
+
price?: string | null;
|
|
22
|
+
taxable?: 'yes' | 'no' | null;
|
|
23
|
+
quantity?: number;
|
|
24
|
+
locationId?: string;
|
|
25
|
+
locationName?: string;
|
|
26
|
+
continueSellingWhenOutOfStock?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export default class ProductInventoryHelpers {
|
|
29
|
+
static buildPlaceholderVariantInventory(input?: PlaceholderVariantInput): PlaceholderVariantInventory[];
|
|
30
|
+
static aggregateQuantityFromPlaceholder(placeholder: PlaceholderVariantInventory[] | null | undefined): number;
|
|
31
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Placeholder variant inventory + aggregation helpers. Pure,
|
|
3
|
+
// dependency-free — used by non-clothing retail product creation flows
|
|
4
|
+
// (cafe items, services, supplies) that need a single-row
|
|
5
|
+
// variantInventory entry without modeling explicit size/color axes.
|
|
6
|
+
//
|
|
7
|
+
// Modelling these items as MULTIVARIANT with a one-row variantInventory
|
|
8
|
+
// keeps every retail SKU under existing ORM productClass values (no new
|
|
9
|
+
// productClass, no SUPPLY misuse) and gives every flow that needs
|
|
10
|
+
// per-location stock the same shape.
|
|
11
|
+
//
|
|
12
|
+
// Tracked items: pass the merchant-entered `quantity` and the
|
|
13
|
+
// merchant's default location id. Indefinite-stock items (cafe /
|
|
14
|
+
// services): pass quantity 0 and `continueSellingWhenOutOfStock: true`
|
|
15
|
+
// — the order processor uses `product.stockStatus = IN_STOCK_UNLIMITED`
|
|
16
|
+
// to skip the actual decrement (see `Product.isUntrackedStock`).
|
|
17
|
+
//
|
|
18
|
+
// Lifted from the ribbn-web-app `lib/utilities/placeholderVariant.ts`
|
|
19
|
+
// shim so backend, mobile, and web share the same construction rule.
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.PLACEHOLDER_VARIANT_ID = void 0;
|
|
22
|
+
exports.PLACEHOLDER_VARIANT_ID = 'default';
|
|
23
|
+
class ProductInventoryHelpers {
|
|
24
|
+
static buildPlaceholderVariantInventory(input = {}) {
|
|
25
|
+
const sku = input.sku === undefined || input.sku === null ? '' : input.sku;
|
|
26
|
+
const price = input.price === undefined || input.price === null ? '' : input.price;
|
|
27
|
+
const taxable = input.taxable === undefined ? null : input.taxable;
|
|
28
|
+
const quantity = input.quantity === undefined ? 0 : input.quantity;
|
|
29
|
+
const locationId = input.locationId === undefined ? '' : input.locationId;
|
|
30
|
+
const locationName = input.locationName === undefined ? '' : input.locationName;
|
|
31
|
+
const continueSellingWhenOutOfStock = input.continueSellingWhenOutOfStock === undefined ? false : input.continueSellingWhenOutOfStock;
|
|
32
|
+
const safeQuantity = Number.isFinite(quantity) ? Math.max(0, Math.floor(Number(quantity))) : 0;
|
|
33
|
+
const variant = {
|
|
34
|
+
id: exports.PLACEHOLDER_VARIANT_ID,
|
|
35
|
+
options: {},
|
|
36
|
+
price: price ? String(price) : '',
|
|
37
|
+
sku: sku ? String(sku) : '',
|
|
38
|
+
taxable: taxable || null,
|
|
39
|
+
continueSellingWhenOutOfStock: Boolean(continueSellingWhenOutOfStock),
|
|
40
|
+
locations: locationId
|
|
41
|
+
? [
|
|
42
|
+
{
|
|
43
|
+
id: locationId,
|
|
44
|
+
name: locationName || '',
|
|
45
|
+
incomingAmount: 0,
|
|
46
|
+
availableAmount: safeQuantity,
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
: [],
|
|
50
|
+
};
|
|
51
|
+
return [variant];
|
|
52
|
+
}
|
|
53
|
+
// Aggregate quantity across the placeholder variant's locations.
|
|
54
|
+
// Mirrors what `ProductFormState.aggregateInventoryLocationsFromVariants`
|
|
55
|
+
// does for the multi-axis case so the product-level `quantity` stays
|
|
56
|
+
// accurate.
|
|
57
|
+
static aggregateQuantityFromPlaceholder(placeholder) {
|
|
58
|
+
if (!Array.isArray(placeholder) || placeholder.length === 0)
|
|
59
|
+
return 0;
|
|
60
|
+
const variant = placeholder[0];
|
|
61
|
+
if (!variant || !variant.locations || variant.locations.length === 0)
|
|
62
|
+
return 0;
|
|
63
|
+
return variant.locations.reduce((acc, loc) => acc + (Number(loc.availableAmount) || 0), 0);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.default = ProductInventoryHelpers;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SKU_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789";
|
|
2
|
+
export declare const DEFAULT_SKU_LENGTH = 10;
|
|
3
|
+
export default class ProductSkuHelpers {
|
|
4
|
+
static generateSku(length?: number): string;
|
|
5
|
+
static effectiveVariantSku(product: {
|
|
6
|
+
sku?: string | null;
|
|
7
|
+
} | null | undefined, variant: {
|
|
8
|
+
sku?: string | null;
|
|
9
|
+
} | null | undefined): string;
|
|
10
|
+
static describeVariantOptions(options: {
|
|
11
|
+
[k: string]: string;
|
|
12
|
+
} | null | undefined): string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SKU and variant-description helpers. Pure, dependency-free — used by
|
|
3
|
+
// retail product creation, label printing, receipt rendering, and any
|
|
4
|
+
// surface that needs to derive a scan-friendly SKU or human-readable
|
|
5
|
+
// variant title from a product / variant pair.
|
|
6
|
+
//
|
|
7
|
+
// Lifted from the ribbn-web-app `lib/utilities/sku.ts` shim so backend,
|
|
8
|
+
// mobile, and web all consume the same alphabet, fallback rules, and
|
|
9
|
+
// variant-description join.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.DEFAULT_SKU_LENGTH = exports.SKU_ALPHABET = void 0;
|
|
12
|
+
// 30-character Crockford-style alphabet that strips visually ambiguous
|
|
13
|
+
// characters (0/O, 1/I/L, U) so a merchant who has to read or type a
|
|
14
|
+
// SKU off a printed tag isn't tripped up by lookalike glyphs.
|
|
15
|
+
//
|
|
16
|
+
// 10 characters from a 30-character alphabet → ~49 bits of entropy.
|
|
17
|
+
// Within a single merchant's catalog the collision probability is
|
|
18
|
+
// effectively zero (you'd need ~10 million products before the chance
|
|
19
|
+
// hits 1%).
|
|
20
|
+
exports.SKU_ALPHABET = 'ABCDEFGHJKMNPQRSTVWXYZ23456789';
|
|
21
|
+
exports.DEFAULT_SKU_LENGTH = 10;
|
|
22
|
+
class ProductSkuHelpers {
|
|
23
|
+
// Generate a short, scan-friendly random SKU. Used as the default
|
|
24
|
+
// value when creating products in flows where the merchant hasn't
|
|
25
|
+
// provided their own SKU. The value is short enough to fit a Code 128
|
|
26
|
+
// barcode on even the smallest practical price-tag labels (≈30×20 mm).
|
|
27
|
+
static generateSku(length = exports.DEFAULT_SKU_LENGTH) {
|
|
28
|
+
const buf = new Uint8Array(length);
|
|
29
|
+
if (typeof globalThis !== 'undefined' &&
|
|
30
|
+
typeof globalThis.crypto !== 'undefined' &&
|
|
31
|
+
typeof globalThis.crypto.getRandomValues === 'function') {
|
|
32
|
+
globalThis.crypto.getRandomValues(buf);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// SSR / test environments without WebCrypto. Not cryptographically
|
|
36
|
+
// strong, but acceptable for a SKU's collision-avoidance use case.
|
|
37
|
+
for (let i = 0; i < length; i = i + 1) {
|
|
38
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let out = '';
|
|
42
|
+
for (let i = 0; i < length; i = i + 1) {
|
|
43
|
+
out = out + exports.SKU_ALPHABET[buf[i] % exports.SKU_ALPHABET.length];
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
// Resolves the SKU that should be encoded on a printed label / scanned
|
|
48
|
+
// barcode for a specific variant. Falls back to the parent product's
|
|
49
|
+
// SKU when the variant doesn't carry its own, matching the convention
|
|
50
|
+
// the retail entry surfaces in the variants table ("Leave a row's SKU
|
|
51
|
+
// blank to fall back to the product SKU").
|
|
52
|
+
static effectiveVariantSku(product, variant) {
|
|
53
|
+
const variantSku = String((variant && variant.sku) || '').trim();
|
|
54
|
+
if (variantSku)
|
|
55
|
+
return variantSku;
|
|
56
|
+
return String((product && product.sku) || '').trim();
|
|
57
|
+
}
|
|
58
|
+
// Builds a human-readable variant title from a variantInventory row's
|
|
59
|
+
// `options` map (e.g. `{ Size: 'M', Color: 'Black' }` → "M / Black").
|
|
60
|
+
// Useful both on printed labels and inside cart line item summaries.
|
|
61
|
+
static describeVariantOptions(options) {
|
|
62
|
+
if (!options)
|
|
63
|
+
return '';
|
|
64
|
+
return Object.keys(options)
|
|
65
|
+
.map((k) => String(options[k] || '').trim())
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join(' / ');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.default = ProductSkuHelpers;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TaxCategoryValue } from '../constants/tax-constants';
|
|
2
|
+
import { TaxCategoryOption } from '../types/pricing-types';
|
|
3
|
+
export default class TaxHelpers {
|
|
4
|
+
static normalizeTaxCategory(taxCategory?: string | null): TaxCategoryValue;
|
|
5
|
+
static getTaxCategoryLabel(taxCategory?: string | null): string;
|
|
6
|
+
static normalizeCountryCode(countryCode?: string | null): string;
|
|
7
|
+
static getCountryTaxRates(countryCode?: string | null): {
|
|
8
|
+
[category: string]: number;
|
|
9
|
+
};
|
|
10
|
+
static getTaxRate(countryCode?: string | null, taxCategory?: string | null): number;
|
|
11
|
+
static formatRatePercent(rate: number): string;
|
|
12
|
+
static getTaxCategoryOptionsWithRates(countryCode?: string | null): TaxCategoryOption[];
|
|
13
|
+
static grossToNetPriceString(grossPrice: string | number | undefined | null, rate: number): string;
|
|
14
|
+
static netToGrossPriceString(netPrice: string | number | undefined | null, rate: number): string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tax_constants_1 = require("../constants/tax-constants");
|
|
4
|
+
// Tax-rate and category helpers. Pure, dependency-free — safe to use in
|
|
5
|
+
// backend services, mobile, and web. Lifted from the ribbn-web-app
|
|
6
|
+
// `lib/utilities/tax.ts` shim so every surface that needs to read or
|
|
7
|
+
// edit a product's tax shape consumes the same normalization.
|
|
8
|
+
class TaxHelpers {
|
|
9
|
+
static normalizeTaxCategory(taxCategory) {
|
|
10
|
+
const raw = String(taxCategory || tax_constants_1.TAX_CATEGORIES.STANDARD).trim();
|
|
11
|
+
return tax_constants_1.TAX_CATEGORY_ALIASES[raw] || tax_constants_1.TAX_CATEGORY_ALIASES[raw.toUpperCase()] || tax_constants_1.TAX_CATEGORIES.STANDARD;
|
|
12
|
+
}
|
|
13
|
+
static getTaxCategoryLabel(taxCategory) {
|
|
14
|
+
return tax_constants_1.TAX_CATEGORY_BASE_LABELS[TaxHelpers.normalizeTaxCategory(taxCategory)];
|
|
15
|
+
}
|
|
16
|
+
static normalizeCountryCode(countryCode) {
|
|
17
|
+
const cc = String(countryCode || '').toUpperCase();
|
|
18
|
+
return tax_constants_1.TAX_CATEGORY_RATES[cc] ? cc : tax_constants_1.FALLBACK_TAX_COUNTRY_CODE;
|
|
19
|
+
}
|
|
20
|
+
static getCountryTaxRates(countryCode) {
|
|
21
|
+
return tax_constants_1.TAX_CATEGORY_RATES[TaxHelpers.normalizeCountryCode(countryCode)];
|
|
22
|
+
}
|
|
23
|
+
static getTaxRate(countryCode, taxCategory) {
|
|
24
|
+
const rates = TaxHelpers.getCountryTaxRates(countryCode);
|
|
25
|
+
const cat = TaxHelpers.normalizeTaxCategory(taxCategory);
|
|
26
|
+
if (cat in rates)
|
|
27
|
+
return rates[cat];
|
|
28
|
+
return rates.STANDARD || 0;
|
|
29
|
+
}
|
|
30
|
+
// Format a rate as a short percent label, e.g. 0.25 -> "25%", 0.055 -> "5.5%".
|
|
31
|
+
static formatRatePercent(rate) {
|
|
32
|
+
if (!rate)
|
|
33
|
+
return '0%';
|
|
34
|
+
const pct = rate * 100;
|
|
35
|
+
if (Math.abs(pct - Math.round(pct)) < 0.05) {
|
|
36
|
+
return `${Math.round(pct)}%`;
|
|
37
|
+
}
|
|
38
|
+
return `${Number(pct.toFixed(2))}%`;
|
|
39
|
+
}
|
|
40
|
+
// Returns option list with rate-aware labels (e.g. "Standard (25%)") for the
|
|
41
|
+
// given country. Useful for select dropdowns in admin product editors.
|
|
42
|
+
static getTaxCategoryOptionsWithRates(countryCode) {
|
|
43
|
+
const rates = TaxHelpers.getCountryTaxRates(countryCode);
|
|
44
|
+
return Object.keys(tax_constants_1.TAX_CATEGORIES).map((key) => {
|
|
45
|
+
const value = tax_constants_1.TAX_CATEGORIES[key];
|
|
46
|
+
const rate = rates[value] || 0;
|
|
47
|
+
const rateLabel = TaxHelpers.formatRatePercent(rate);
|
|
48
|
+
const baseLabel = tax_constants_1.TAX_CATEGORY_BASE_LABELS[value];
|
|
49
|
+
const label = value === tax_constants_1.TAX_CATEGORIES.EXEMPT ? `${baseLabel} (no tax)` : `${baseLabel} (${rateLabel})`;
|
|
50
|
+
return { value, label, rate, rateLabel };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Convert a tax-inclusive (gross) price string to a tax-exclusive (net)
|
|
54
|
+
// price string formatted with 2 decimals. Returns the original string if
|
|
55
|
+
// it can't be parsed or if rate is 0.
|
|
56
|
+
static grossToNetPriceString(grossPrice, rate) {
|
|
57
|
+
const gross = typeof grossPrice === 'number' ? grossPrice : parseFloat(String(grossPrice == null ? '' : grossPrice));
|
|
58
|
+
if (!Number.isFinite(gross) || gross <= 0) {
|
|
59
|
+
return String(grossPrice == null ? '' : grossPrice);
|
|
60
|
+
}
|
|
61
|
+
if (!rate)
|
|
62
|
+
return gross.toFixed(2);
|
|
63
|
+
return (gross / (1 + rate)).toFixed(2);
|
|
64
|
+
}
|
|
65
|
+
// Convert a stored net price back to a gross price for display in inputs.
|
|
66
|
+
static netToGrossPriceString(netPrice, rate) {
|
|
67
|
+
const net = typeof netPrice === 'number' ? netPrice : parseFloat(String(netPrice == null ? '' : netPrice));
|
|
68
|
+
if (!Number.isFinite(net) || net <= 0) {
|
|
69
|
+
return String(netPrice == null ? '' : netPrice);
|
|
70
|
+
}
|
|
71
|
+
if (!rate)
|
|
72
|
+
return net.toFixed(2);
|
|
73
|
+
return (net * (1 + rate)).toFixed(2);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.default = TaxHelpers;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type CommissionPolicyContext = {
|
|
2
|
+
taxableAmount: number;
|
|
3
|
+
originalRate: number;
|
|
4
|
+
marketplaceRate: number;
|
|
5
|
+
};
|
|
6
|
+
export type CommissionPolicyResult = {
|
|
7
|
+
originalProductCommissionAmount: number;
|
|
8
|
+
marketplaceCommissionAmount: number;
|
|
9
|
+
managedMerchantProceedsAmount: number;
|
|
10
|
+
sellerProceedsAmount: number;
|
|
11
|
+
};
|
|
12
|
+
export type CommissionPolicyFeeContext = {
|
|
13
|
+
stripeFee: number;
|
|
14
|
+
ribbnFee: number;
|
|
15
|
+
};
|
|
16
|
+
export interface CommissionPolicy {
|
|
17
|
+
readonly basis: string;
|
|
18
|
+
calculate(context: CommissionPolicyContext, feeContext: CommissionPolicyFeeContext): CommissionPolicyResult;
|
|
19
|
+
}
|
|
20
|
+
export declare const roundCommissionAmount: (value: number) => number;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Strategy interface for marketplace commission calculation. Concrete
|
|
3
|
+
// implementations live in sibling files. Callers should depend on this
|
|
4
|
+
// abstraction (and the registered `basis` string) rather than any concrete
|
|
5
|
+
// class so new bases can be added without modifying existing code paths.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.roundCommissionAmount = void 0;
|
|
8
|
+
const roundCommissionAmount = (value) => Number(Number(value || 0).toFixed(2));
|
|
9
|
+
exports.roundCommissionAmount = roundCommissionAmount;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.has = exports.get = exports.register = void 0;
|
|
4
|
+
const marketplace_constants_1 = require("../../constants/marketplace-constants");
|
|
5
|
+
const LineNetCommissionPolicy_1 = require("./LineNetCommissionPolicy");
|
|
6
|
+
const OriginalCommissionShareCommissionPolicy_1 = require("./OriginalCommissionShareCommissionPolicy");
|
|
7
|
+
// Process-wide registry for marketplace commission policies. Built-in
|
|
8
|
+
// strategies are registered eagerly so the default exported registry is
|
|
9
|
+
// usable without setup. New bases can be added at runtime via
|
|
10
|
+
// `register(policy)` to satisfy the open/closed principle.
|
|
11
|
+
const REGISTRY = {};
|
|
12
|
+
const register = (policy) => {
|
|
13
|
+
REGISTRY[policy.basis] = policy;
|
|
14
|
+
};
|
|
15
|
+
exports.register = register;
|
|
16
|
+
const get = (basis) => REGISTRY[basis] || REGISTRY[marketplace_constants_1.DEFAULT_MARKETPLACE_COMMISSION_BASIS];
|
|
17
|
+
exports.get = get;
|
|
18
|
+
const has = (basis) => Boolean(REGISTRY[basis]);
|
|
19
|
+
exports.has = has;
|
|
20
|
+
(0, exports.register)(new LineNetCommissionPolicy_1.default());
|
|
21
|
+
(0, exports.register)(new OriginalCommissionShareCommissionPolicy_1.default());
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { FeeAllocationContext, FeeAllocationPolicy } from './FeeAllocationPolicy';
|
|
2
|
+
export default class EvenPerSellerStripeFeePolicy implements FeeAllocationPolicy {
|
|
3
|
+
readonly basis: "EVEN_PER_SELLER";
|
|
4
|
+
allocate({ totalFeeAmount, basket }: FeeAllocationContext): MarketplaceFeeAllocation[];
|
|
5
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const marketplace_constants_1 = require("../../constants/marketplace-constants");
|
|
4
|
+
const FeeAllocationPolicy_1 = require("./FeeAllocationPolicy");
|
|
5
|
+
// Mirrors the existing backend `applyStripeFeeRecoupBump` semantics:
|
|
6
|
+
// merchantShare = totalFee / numUniqueManagedSellersInBasket
|
|
7
|
+
// linePerSellerCt = lines for that seller
|
|
8
|
+
// per-line amount = merchantShare / linePerSellerCt
|
|
9
|
+
//
|
|
10
|
+
// Lines whose `lineNetAmount` is non-positive are skipped so we never spread
|
|
11
|
+
// a fee onto a freebie line, matching the legacy guard against zero-priced
|
|
12
|
+
// items.
|
|
13
|
+
class EvenPerSellerStripeFeePolicy {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.basis = marketplace_constants_1.MARKETPLACE_FEE_ALLOCATION_BASES.evenPerSeller;
|
|
16
|
+
}
|
|
17
|
+
allocate({ totalFeeAmount, basket }) {
|
|
18
|
+
if (!Array.isArray(basket) || basket.length === 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const safeTotal = Number(totalFeeAmount);
|
|
22
|
+
if (!Number.isFinite(safeTotal) || safeTotal <= 0) {
|
|
23
|
+
return basket.map((line) => ({ lineKey: line.lineKey, amount: 0 }));
|
|
24
|
+
}
|
|
25
|
+
const eligibleLines = basket.filter((line) => Number.isFinite(Number(line === null || line === void 0 ? void 0 : line.lineNetAmount)) &&
|
|
26
|
+
Number(line.lineNetAmount) > 0 &&
|
|
27
|
+
Boolean(line.sellerMerchantId));
|
|
28
|
+
if (eligibleLines.length === 0) {
|
|
29
|
+
return basket.map((line) => ({ lineKey: line.lineKey, amount: 0 }));
|
|
30
|
+
}
|
|
31
|
+
const sellerIds = Array.from(new Set(eligibleLines.map((line) => String(line.sellerMerchantId))));
|
|
32
|
+
const merchantShare = safeTotal / sellerIds.length;
|
|
33
|
+
const lineCountByVendor = sellerIds.reduce((acc, sellerId) => {
|
|
34
|
+
acc[sellerId] = eligibleLines.filter((line) => line.sellerMerchantId === sellerId).length;
|
|
35
|
+
return acc;
|
|
36
|
+
}, {});
|
|
37
|
+
const allocations = basket.map((line) => {
|
|
38
|
+
const vendorLineCount = lineCountByVendor[String(line.sellerMerchantId)] || 0;
|
|
39
|
+
const isEligible = vendorLineCount > 0 && Number.isFinite(Number(line === null || line === void 0 ? void 0 : line.lineNetAmount)) && Number(line.lineNetAmount) > 0;
|
|
40
|
+
const amount = isEligible ? (0, FeeAllocationPolicy_1.roundFeeAmount)(merchantShare / vendorLineCount) : 0;
|
|
41
|
+
return { lineKey: line.lineKey, amount };
|
|
42
|
+
});
|
|
43
|
+
return (0, FeeAllocationPolicy_1.reconcileAllocationsToTotal)(allocations, safeTotal);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.default = EvenPerSellerStripeFeePolicy;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FeeAllocationContext = {
|
|
2
|
+
totalFeeAmount: number;
|
|
3
|
+
basket: MarketplaceFeeBasketLine[];
|
|
4
|
+
};
|
|
5
|
+
export interface FeeAllocationPolicy {
|
|
6
|
+
readonly basis: string;
|
|
7
|
+
allocate(context: FeeAllocationContext): MarketplaceFeeAllocation[];
|
|
8
|
+
}
|
|
9
|
+
export declare const roundFeeAmount: (value: number) => number;
|
|
10
|
+
export declare const reconcileAllocationsToTotal: (allocations: MarketplaceFeeAllocation[], totalFeeAmount: number) => MarketplaceFeeAllocation[];
|