@stacksjs/commerce 0.75.112 → 0.75.113
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/README.md +15 -0
- package/dist/auctions/bids/fetch.js +1 -0
- package/dist/auctions/bids/index.js +1 -0
- package/dist/auctions/bids/place.js +1 -0
- package/dist/auctions/engine/anti-snipe.js +1 -0
- package/dist/auctions/engine/increments.js +1 -0
- package/dist/auctions/engine/index.js +1 -0
- package/dist/auctions/engine/proxy.js +1 -0
- package/dist/auctions/engine/winners.js +1 -0
- package/dist/auctions/index.js +1 -0
- package/dist/auctions/lots/fetch.js +1 -0
- package/dist/auctions/lots/index.js +1 -0
- package/dist/auctions/lots/lifecycle.js +1 -0
- package/dist/auctions/notifications.js +1 -0
- package/dist/auctions/pledges/index.js +1 -0
- package/dist/auctions/realtime.js +1 -0
- package/dist/auctions/rules.js +1 -0
- package/dist/auctions/types.js +0 -0
- package/dist/carts/destroy.js +1 -0
- package/dist/carts/fetch.js +1 -0
- package/dist/carts/index.js +1 -0
- package/dist/carts/store.js +1 -0
- package/dist/carts/update.js +1 -0
- package/dist/catalog.js +1 -2
- package/dist/coupons/destroy.js +1 -0
- package/dist/coupons/fetch.js +1 -0
- package/dist/coupons/index.js +1 -0
- package/dist/coupons/store.js +1 -0
- package/dist/coupons/update.js +7 -0
- package/dist/customers/destroy.js +1 -0
- package/dist/customers/fetch.js +1 -0
- package/dist/customers/index.js +1 -0
- package/dist/customers/store.js +1 -0
- package/dist/customers/update.js +1 -0
- package/dist/devices/destroy.js +1 -0
- package/dist/devices/export.js +1 -0
- package/dist/devices/fetch.js +1 -0
- package/dist/devices/index.js +1 -0
- package/dist/devices/store.js +1 -0
- package/dist/devices/update.js +1 -0
- package/dist/errors/destroy.js +1 -0
- package/dist/errors/fetch.js +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/errors/update.js +1 -0
- package/dist/gift-cards/destroy.js +1 -0
- package/dist/gift-cards/fetch.js +1 -0
- package/dist/gift-cards/index.js +1 -0
- package/dist/gift-cards/store.js +1 -0
- package/dist/gift-cards/update.js +10 -0
- package/dist/imports/account-mapping.js +1 -0
- package/dist/imports/account-types.js +0 -0
- package/dist/imports/account-writer.js +1 -0
- package/dist/imports/admin-http.js +1 -0
- package/dist/imports/credentials.js +1 -0
- package/dist/imports/http.js +1 -0
- package/dist/imports/identity.js +1 -0
- package/dist/imports/index.js +1 -0
- package/dist/imports/money.js +1 -0
- package/dist/imports/shopify-admin.js +1 -0
- package/dist/imports/shopify.js +1 -0
- package/dist/imports/shopware-admin.js +1 -0
- package/dist/imports/shopware.js +1 -0
- package/dist/imports/text.js +1 -0
- package/dist/imports/types.js +0 -0
- package/dist/imports/woocommerce-admin.js +1 -0
- package/dist/imports/woocommerce.js +1 -0
- package/dist/imports/writer.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -28
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.js +1 -0
- package/dist/money.js +1 -2
- package/dist/orders/destroy.js +1 -0
- package/dist/orders/events.js +1 -0
- package/dist/orders/export.js +1 -0
- package/dist/orders/fetch.js +1 -0
- package/dist/orders/guards.js +1 -0
- package/dist/orders/index.js +1 -0
- package/dist/orders/place-order.js +1 -0
- package/dist/orders/store.js +1 -0
- package/dist/orders/totals.js +1 -0
- package/dist/orders/transition.js +1 -0
- package/dist/orders/update.js +1 -0
- package/dist/orders/webhook.js +1 -0
- package/dist/payments/destroy.js +1 -0
- package/dist/payments/fetch.js +1 -0
- package/dist/payments/index.js +1 -0
- package/dist/payments/store.js +1 -0
- package/dist/payments/update.js +8 -0
- package/dist/product.d.ts +1 -0
- package/dist/product.js +0 -0
- package/dist/products/categories/destroy.js +1 -0
- package/dist/products/categories/fetch.js +1 -0
- package/dist/products/categories/index.js +1 -0
- package/dist/products/categories/store.js +1 -0
- package/dist/products/categories/types.js +0 -0
- package/dist/products/categories/update.js +1 -0
- package/dist/products/index.js +1 -0
- package/dist/products/items/destroy.js +1 -0
- package/dist/products/items/fetch.js +1 -0
- package/dist/products/items/index.d.ts +1 -0
- package/dist/products/items/index.js +1 -0
- package/dist/products/items/store.js +1 -0
- package/dist/products/items/update.d.ts +2 -0
- package/dist/products/items/update.js +1 -0
- package/dist/products/manufacturers/destroy.js +1 -0
- package/dist/products/manufacturers/fetch.js +1 -0
- package/dist/products/manufacturers/index.js +1 -0
- package/dist/products/manufacturers/store.js +1 -0
- package/dist/products/manufacturers/update.js +1 -0
- package/dist/products/reviews/destroy.js +1 -0
- package/dist/products/reviews/fetch.js +1 -0
- package/dist/products/reviews/index.js +1 -0
- package/dist/products/reviews/stats.js +1 -0
- package/dist/products/reviews/store.js +1 -0
- package/dist/products/reviews/update.js +1 -0
- package/dist/products/units/destroy.js +1 -0
- package/dist/products/units/fetch.js +1 -0
- package/dist/products/units/index.js +1 -0
- package/dist/products/units/store.js +1 -0
- package/dist/products/units/types.js +0 -0
- package/dist/products/units/update.js +1 -0
- package/dist/products/variants/destroy.js +1 -0
- package/dist/products/variants/fetch.js +1 -0
- package/dist/products/variants/index.js +1 -0
- package/dist/products/variants/store.js +1 -0
- package/dist/products/variants/update.js +1 -0
- package/dist/receipts/destroy.js +1 -0
- package/dist/receipts/export.d.ts +1 -0
- package/dist/receipts/export.js +0 -0
- package/dist/receipts/fetch.js +1 -0
- package/dist/receipts/index.js +1 -0
- package/dist/receipts/printer.d.ts +37 -0
- package/dist/receipts/printer.js +1 -0
- package/dist/receipts/store.js +1 -0
- package/dist/receipts/timestamp.js +1 -0
- package/dist/receipts/update.js +1 -0
- package/dist/refund-allocation.js +1 -2
- package/dist/register.js +1 -2
- package/dist/releases.js +1 -2
- package/dist/sales-tax.js +1 -2
- package/dist/shippings/couriers/destroy.js +1 -0
- package/dist/shippings/couriers/fetch.js +1 -0
- package/dist/shippings/couriers/index.js +1 -0
- package/dist/shippings/couriers/store.js +1 -0
- package/dist/shippings/couriers/update.js +1 -0
- package/dist/shippings/delivery-routes/destroy.js +1 -0
- package/dist/shippings/delivery-routes/fetch.js +1 -0
- package/dist/shippings/delivery-routes/index.js +1 -0
- package/dist/shippings/delivery-routes/store.js +1 -0
- package/dist/shippings/delivery-routes/update.js +1 -0
- package/dist/shippings/delivery-routes/validate-write.js +1 -0
- package/dist/shippings/digital-deliveries/destroy.js +1 -0
- package/dist/shippings/digital-deliveries/fetch.js +1 -0
- package/dist/shippings/digital-deliveries/index.js +1 -0
- package/dist/shippings/digital-deliveries/store.js +1 -0
- package/dist/shippings/digital-deliveries/update.js +1 -0
- package/dist/shippings/geocoding/index.js +1 -0
- package/dist/shippings/geocoding/nominatim.js +1 -0
- package/dist/shippings/geocoding/types.js +1 -0
- package/dist/shippings/index.js +1 -0
- package/dist/shippings/license-keys/destroy.js +1 -0
- package/dist/shippings/license-keys/fetch.js +1 -0
- package/dist/shippings/license-keys/index.js +1 -0
- package/dist/shippings/license-keys/store.js +1 -0
- package/dist/shippings/license-keys/update.js +1 -0
- package/dist/shippings/license-keys/validate-write.js +1 -0
- package/dist/shippings/shipping-methods/destroy.js +1 -0
- package/dist/shippings/shipping-methods/fetch.js +1 -0
- package/dist/shippings/shipping-methods/index.js +1 -0
- package/dist/shippings/shipping-methods/store.js +1 -0
- package/dist/shippings/shipping-methods/update.js +1 -0
- package/dist/shippings/shipping-rates/destroy.js +1 -0
- package/dist/shippings/shipping-rates/fetch.js +1 -0
- package/dist/shippings/shipping-rates/index.js +1 -0
- package/dist/shippings/shipping-rates/store.js +1 -0
- package/dist/shippings/shipping-rates/update.js +1 -0
- package/dist/shippings/shipping-rates/validate-write.js +1 -0
- package/dist/shippings/shipping-zones/destroy.js +1 -0
- package/dist/shippings/shipping-zones/fetch.js +1 -0
- package/dist/shippings/shipping-zones/index.js +1 -0
- package/dist/shippings/shipping-zones/store.js +1 -0
- package/dist/shippings/shipping-zones/update.js +1 -0
- package/dist/shippings/tracking/events.js +1 -0
- package/dist/shippings/tracking/geo.js +1 -0
- package/dist/shippings/tracking/index.js +1 -0
- package/dist/shippings/tracking/ping.js +1 -0
- package/dist/shippings/tracking/stops.js +1 -0
- package/dist/shippings/write-data.js +1 -0
- package/dist/tax/breakdown.js +1 -0
- package/dist/tax/destroy.js +1 -0
- package/dist/tax/fetch.js +1 -0
- package/dist/tax/index.js +1 -0
- package/dist/tax/store.js +1 -0
- package/dist/tax/types.js +0 -0
- package/dist/tax/update.js +1 -0
- package/dist/tax-input.d.ts +7 -0
- package/dist/tax-input.js +1 -0
- package/dist/tests/fixtures/returning-all-writes.d.ts +1 -0
- package/dist/tests/fixtures/returning-all-writes.js +1 -0
- package/dist/tests/fixtures/soft-delete-counts.d.ts +1 -0
- package/dist/tests/fixtures/soft-delete-counts.js +1 -0
- package/dist/tests/fixtures/write-counts.d.ts +1 -0
- package/dist/tests/fixtures/write-counts.js +8 -0
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.js +1 -0
- package/dist/types.js +0 -0
- package/dist/utils/inserted-id.js +1 -0
- package/dist/utils/inventory-adjustment.js +5 -0
- package/dist/utils/model-row.js +1 -0
- package/dist/utils/mutation-count.js +1 -0
- package/dist/utils/typed-stats.js +1 -0
- package/dist/waitlists/index.js +1 -0
- package/dist/waitlists/products/destroy.js +1 -0
- package/dist/waitlists/products/export.d.ts +41 -0
- package/dist/waitlists/products/export.js +1 -0
- package/dist/waitlists/products/fetch.js +1 -0
- package/dist/waitlists/products/index.js +1 -0
- package/dist/waitlists/products/store.js +1 -0
- package/dist/waitlists/products/update.js +1 -0
- package/dist/waitlists/products/write-data.js +1 -0
- package/dist/waitlists/restaurant/destroy.js +1 -0
- package/dist/waitlists/restaurant/export.d.ts +39 -0
- package/dist/waitlists/restaurant/export.js +1 -0
- package/dist/waitlists/restaurant/fetch.js +1 -0
- package/dist/waitlists/restaurant/index.js +1 -0
- package/dist/waitlists/restaurant/store.js +1 -0
- package/dist/waitlists/restaurant/update.js +1 -0
- package/dist/waitlists/restaurant/write-data.js +1 -0
- package/package.json +13 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{addressLine,amountOrZero,currencyCode,gmtTimestamp,normalizeEmail,personName,phoneOf,quantityOf,signedMinor,unitFromTotal}from"./account-mapping";import{adminRequest}from"./admin-http";import{ACCOUNT_CREDENTIAL_HELP,credentialSecrets}from"./credentials";import{CatalogFetchError,normalizeStoreUrl}from"./http";import{currencyExponent,PriceFormatError}from"./money";import{cleanName,optionalString}from"./text";const PAGE_SIZE=100,MAX_PAGES=1e4,API_PATH="/wp-json/wc/v3",CREDENTIAL_HELP=`Check WOOCOMMERCE_CONSUMER_KEY and WOOCOMMERCE_CONSUMER_SECRET, and that the key has Read permission. If the key is right and every request is a 401, the web server may be stripping the Authorization header (common with Apache and CGI PHP). ${ACCOUNT_CREDENTIAL_HELP.woocommerce}`,HINT="Is WooCommerce installed with the REST API reachable at /wp-json/wc/v3, and pretty permalinks enabled?",WOO_STATUS={pending:"PENDING","on-hold":"PENDING",processing:"PROCESSING",completed:"DELIVERED",cancelled:"CANCELLED",failed:"CANCELLED",refunded:"REFUNDED"},NOT_ORDERS=new Set(["checkout-draft","auto-draft","trash"]);export function wooOrderStatus(status){const mapped=WOO_STATUS[(status??"").toLowerCase()];return mapped?{status:mapped,mapped:!0}:{status:"PENDING",mapped:!1}}function httpsUrl(value){const raw=optionalString(value);if(!raw)return null;try{return new URL(raw).protocol==="https:"?raw:null}catch{return null}}export function mapWooCustomer(raw){const externalId=raw?.id===void 0||raw.id===null?"":String(raw.id),warnings=[],email=normalizeEmail(raw?.email)??normalizeEmail(raw?.billing?.email);if(!externalId||!email){warnings.push({externalId:externalId||void 0,message:"skipped: the customer has no valid email address, which Stacks customers are keyed by"});return{record:null,warnings}}return{record:{source:"woocommerce",externalId,email,name:personName(raw.first_name,raw.last_name)??personName(raw.billing?.first_name,raw.billing?.last_name)??email,phone:phoneOf(raw.billing?.phone,raw.shipping?.phone),active:null,totalSpentMinor:null,currency:null,lastOrderAt:null,avatarUrl:httpsUrl(raw.avatar_url),createdAt:gmtTimestamp(raw.date_created_gmt)},warnings}}function shippingAddressOf(address){if(!address||!optionalString(address.address_1))return null;return addressLine(personName(address.first_name,address.last_name),address.company,address.address_1,address.address_2,address.city,[address.state,address.postcode].map((part)=>cleanName(part)).filter(Boolean).join(" "),address.country)}export function mapWooOrder(raw,context={}){const externalId=raw?.id===void 0||raw.id===null?"":String(raw.id),warnings=[];if(!externalId){warnings.push({message:"skipped: a WooCommerce order has no id"});return{record:null,warnings}}const sourceStatus=(raw.status??"").toLowerCase();if(NOT_ORDERS.has(sourceStatus)){warnings.push({externalId,message:`skipped: status "${sourceStatus}" is not a placed order`});return{record:null,warnings}}const currency=currencyCode(raw.currency)??currencyCode(context.currency);if(!currency){warnings.push({externalId,message:"skipped: the order names no currency; pass --currency"});return{record:null,warnings}}const exponent=currencyExponent(currency),amount=(value,label)=>amountOrZero(value,exponent,label,externalId,warnings),lines=[];for(const item of raw.line_items??[]){const lineId=item?.id===void 0||item.id===null?"":String(item.id),quantity=quantityOf(item?.quantity),name=cleanName(item?.name)||`Line ${lineId}`;if(!lineId||quantity===null){warnings.push({externalId,message:`line "${name}" has no id or quantity; left out`});continue}const subtotal=amount(item.subtotal??item.total,`line "${name}" subtotal`);lines.push({externalId:lineId,productExternalId:item.product_id?String(item.product_id):null,variantExternalId:item.variation_id?String(item.variation_id):null,name,sku:optionalString(item.sku),quantity,unitPriceMinor:unitFromTotal(subtotal,quantity)})}let feeDiscount=0;for(const fee of raw.fee_lines??[]){const name=cleanName(fee?.name)||"Fee";let minor=null;try{minor=signedMinor(fee?.total,exponent)}catch(error){if(!(error instanceof PriceFormatError))throw error;warnings.push({externalId,message:`fee "${name}": ${error.message}; left out`})}if(minor===null||minor===0)continue;if(minor<0){feeDiscount+=-minor;continue}lines.push({externalId:`fee-${fee?.id??lines.length}`,productExternalId:null,variantExternalId:null,name,sku:null,quantity:1,unitPriceMinor:minor})}const{status,mapped}=wooOrderStatus(sourceStatus);if(!mapped)warnings.push({externalId,message:`WooCommerce status "${sourceStatus||"none"}" has no Stacks equivalent; imported as ${status}`});const customerId=raw.customer_id?String(raw.customer_id):null,email=normalizeEmail(raw.billing?.email);let refundedMinor=0;for(const refund of raw.refunds??[])try{refundedMinor+=Math.abs(signedMinor(refund?.total,exponent)??0)}catch(error){if(!(error instanceof PriceFormatError))throw error;warnings.push({externalId,message:`refund ${refund?.id??""}: ${error.message}; not counted`})}return{record:{source:"woocommerce",externalId,number:optionalString(raw.number)?`#${optionalString(raw.number)}`:null,currency,status,sourceStatus:sourceStatus||"none",totalMinor:amount(raw.total,"total"),taxMinor:amount(raw.total_tax,"tax"),discountMinor:amount(raw.discount_total,"discounts")+feeDiscount,shippingMinor:amount(raw.shipping_total,"shipping"),tipMinor:0,customer:email||customerId?{externalId:customerId,email,name:personName(raw.billing?.first_name,raw.billing?.last_name),phone:phoneOf(raw.billing?.phone)}:null,shippingAddress:shippingAddressOf(raw.shipping),note:optionalString(raw.customer_note),placedAt:gmtTimestamp(raw.date_created_gmt),lines,refundedMinor},warnings}}export function wooAdminBase(storeUrl,adminUrl){const base=normalizeStoreUrl(adminUrl??storeUrl);if(!base.startsWith("https://"))throw TypeError(`${base} is not HTTPS. WooCommerce REST API keys are sent with HTTP Basic auth, which is only safe (and only accepted by WooCommerce) over HTTPS. Use the store's https:// address.`);return base}export function wooAuthorization(consumerKey,consumerSecret){return`Basic ${Buffer.from(`${consumerKey}:${consumerSecret}`,"utf8").toString("base64")}`}async function*wooResource(storeUrl,options,resource,map){if(options.credentials.source!=="woocommerce")throw TypeError("The WooCommerce importer needs WooCommerce credentials (WOOCOMMERCE_CONSUMER_KEY and WOOCOMMERCE_CONSUMER_SECRET).");const base=wooAdminBase(storeUrl,options.adminUrl),{consumerKey,consumerSecret}=options.credentials,authorization=wooAuthorization(consumerKey,consumerSecret),request={fetch:options.fetch??fetch,headers:{authorization},secrets:[...credentialSecrets(options.credentials),authorization.slice(6)],credentialHelp:CREDENTIAL_HELP,hint:HINT,sleep:options.sleep},limit=options.limit,pageSize=limit&&limit>0?Math.min(PAGE_SIZE,limit):PAGE_SIZE,seen=new Set;let emitted=0;for(let page=1;page<=MAX_PAGES;page++){const url=`${base}${API_PATH}/${resource}?per_page=${pageSize}&page=${page}&orderby=id&order=asc`,{data,headers}=await adminRequest(url,request);if(!Array.isArray(data))throw new CatalogFetchError(url,`${url} did not return a list of ${resource}. ${HINT}`);const raw=data;if(raw.length===0)return;const ids=raw.map((entry)=>String(entry?.id));if(page>1&&ids.every((id)=>seen.has(id)))throw new CatalogFetchError(url,`${url} repeated ${resource} from an earlier page; the store is not paginating.`);ids.forEach((id)=>seen.add(id));const items=[],warnings=[];for(const entry of raw){if(limit&&emitted>=limit)break;const mapped=map(entry);warnings.push(...mapped.warnings);if(mapped.record){items.push(mapped.record);emitted++}}yield{url,items,warnings};const header=headers.get("x-wp-totalpages")?.trim(),totalPages=header?Number(header):Number.NaN;if(limit&&emitted>=limit)return;if(Number.isInteger(totalPages)&&page>=totalPages)return}throw new CatalogFetchError(`${base}${API_PATH}/${resource}`,`Stopped after ${MAX_PAGES} pages of ${base}${API_PATH}/${resource} without reaching the end.`)}export const wooCommerceAccountAdapter={name:"woocommerce",customers:(storeUrl,options)=>wooResource(storeUrl,options,"customers",mapWooCustomer),orders:(storeUrl,options)=>wooResource(storeUrl,options,"orders",(raw)=>mapWooOrder(raw,{currency:options.currency}))};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{slug}from"@stacksjs/strings";import{CatalogFetchError,fetchJson,normalizeStoreUrl}from"./http";import{currencyExponent,PriceFormatError,rescaleMinor}from"./money";import{cleanName,optionalString}from"./text";const PAGE_SIZE=100,MAX_PAGES=1000,API_PATH="/wp-json/wc/store/v1/products",HINT="Is WooCommerce 6+ installed with the Store API reachable at /wp-json?";export function wooPrices(prices){const currency=optionalString(prices?.currency_code)?.toUpperCase()??null,declared=Number(prices?.currency_minor_unit),fromExponent=Number.isInteger(declared)&&declared>=0?declared:2,toExponent=currencyExponent(currency),priceMinor=rescaleMinor(prices?.price,fromExponent,toExponent),regularMinor=rescaleMinor(prices?.regular_price,fromExponent,toExponent);return{currency,priceMinor,compareAtMinor:regularMinor!==null&&priceMinor!==null&®ularMinor>priceMinor?regularMinor:null}}function safePrices(payload,externalId,warnings){try{return wooPrices(payload.prices)}catch(error){if(!(error instanceof PriceFormatError))throw error;warnings.push({externalId,message:`${error.message}; left unpriced`});return{currency:optionalString(payload.prices?.currency_code)?.toUpperCase()??null,priceMinor:null,compareAtMinor:null}}}function terms(list){return(list??[]).map((term)=>{const name=cleanName(term?.name);return{name,slug:optionalString(term?.slug)??slug(name)}}).filter((term)=>term.name&&term.slug)}export function wooSlug(value){const raw=optionalString(value);if(!raw||!raw.includes("%"))return raw;try{return decodeURIComponent(raw)}catch{return raw}}function inventoryOf(payload){return typeof payload.low_stock_remaining==="number"?Math.max(0,Math.trunc(payload.low_stock_remaining)):null}function availableOf(payload){return payload.is_in_stock!==!1&&payload.is_purchasable!==!1}export function mapWooProduct(raw,context={}){if(raw?.id===void 0||raw.id===null||raw.id==="")throw TypeError("WooCommerce product has no id");const externalId=String(raw.id),warnings=[],name=cleanName(raw.name);if(!name)throw TypeError(`WooCommerce product ${externalId} has no name`);const own=safePrices(raw,externalId,warnings),variable=raw.type==="variable"&&(raw.variations?.length??0)>0,optionAttributes=variable?(raw.attributes??[]).filter((attribute)=>attribute.has_variations!==!1):[],optionNames=optionAttributes.map((attribute)=>cleanName(attribute.name)).filter(Boolean);let variants,currency=own.currency;if(variable)variants=(raw.variations??[]).map((entry)=>{const variationId=String(entry.id),optionValues=optionAttributes.map((attribute)=>{const label=cleanName(attribute.name).toLowerCase(),taxonomy=(attribute.taxonomy??"").toLowerCase(),picked=(entry.attributes??[]).find((value)=>{const key=cleanName(value.name).toLowerCase();return key===label||taxonomy!==""&&key===taxonomy}),value=optionalString(picked?.value);if(!value)return"Any";const term=(attribute.terms??[]).find((candidate)=>candidate.slug===value);return term?cleanName(term.name):cleanName(value)}),detail=context.variations?.get(variationId);if(!detail){warnings.push({externalId,message:`variation ${variationId} could not be fetched; imported without a price`});return{externalId:variationId,title:optionValues.join(" / ")||name,sku:null,priceMinor:null,compareAtMinor:null,available:!1,inventory:null,optionValues}}const prices=safePrices(detail,variationId,warnings);currency??=prices.currency;return{externalId:variationId,title:optionValues.join(" / ")||cleanName(detail.name)||name,sku:optionalString(detail.sku),priceMinor:prices.priceMinor,compareAtMinor:prices.compareAtMinor,available:availableOf(detail),inventory:inventoryOf(detail),optionValues}});else variants=[{externalId,title:name,sku:optionalString(raw.sku),priceMinor:own.priceMinor,compareAtMinor:own.compareAtMinor,available:availableOf(raw),inventory:inventoryOf(raw),optionValues:[]}];const brand=terms(raw.brands)[0];return{product:{source:"woocommerce",externalId,name,handle:wooSlug(raw.slug)??slug(name),descriptionHtml:optionalString(raw.description)??optionalString(raw.short_description),vendor:brand?brand.name:null,categories:terms(raw.categories),tags:terms(raw.tags).map((tag)=>tag.name),currency,images:(raw.images??[]).filter((image)=>optionalString(image?.src)).map((image)=>({src:optionalString(image.src),alt:optionalString(image.alt)})),optionNames:variable?optionNames:[],variants,hasOptions:variable,url:optionalString(raw.permalink)},warnings}}async function listPage(url,fetcher){const{data,headers}=await fetchJson(url,fetcher,HINT);if(!Array.isArray(data))throw new CatalogFetchError(url,`${url} did not return a product list. ${HINT}`);const header=headers.get("x-wp-totalpages")?.trim(),total=header?Number(header):Number.NaN;return{url,items:data,totalPages:Number.isInteger(total)&&total>=0?total:null}}async function fetchVariations(base,parentIds,fetcher){const found=new Map;if(parentIds.length===0)return found;for(let page=1;page<=MAX_PAGES;page++){const url=`${base}${API_PATH}?type=variation&parent=${parentIds.join(",")}&per_page=${PAGE_SIZE}&page=${page}`,result=await listPage(url,fetcher);for(const item of result.items)found.set(String(item.id),item);if(result.items.length===0||result.totalPages!==null&&page>=result.totalPages)break}return found}async function*wooPages(storeUrl,options={}){const base=normalizeStoreUrl(storeUrl),fetcher=options.fetch??fetch,limit=options.limit,pageSize=limit&&limit>0?Math.min(PAGE_SIZE,limit):PAGE_SIZE,seen=new Set;let emitted=0;for(let page=1;page<=MAX_PAGES;page++){const url=`${base}${API_PATH}?per_page=${pageSize}&page=${page}`,result=await listPage(url,fetcher);if(result.items.length===0)return;const ids=result.items.map((item)=>String(item?.id));if(page>1&&ids.every((id)=>seen.has(id)))throw new CatalogFetchError(url,`${url} repeated products from an earlier page; the store is not paginating. Use --limit to import the first page only.`);ids.forEach((id)=>seen.add(id));const take=limit?result.items.slice(0,Math.max(0,limit-emitted)):result.items,warnings=[],parents=take.filter((item)=>item?.type==="variable"&&(item.variations?.length??0)>0).map((item)=>String(item.id));let variations=new Map;try{variations=await fetchVariations(base,parents,fetcher)}catch(error){if(!(error instanceof CatalogFetchError))throw error;warnings.push({message:`variations could not be fetched (${error.message}); variable products are imported without variant prices`})}const products=[];for(const entry of take)try{const mapped=mapWooProduct(entry,{variations});products.push(mapped.product);warnings.push(...mapped.warnings);emitted++}catch(error){warnings.push({externalId:entry?.id===void 0?void 0:String(entry.id),message:`skipped: ${error.message}`})}yield{url,products,warnings};if(limit&&emitted>=limit)return;if(result.totalPages!==null&&page>=result.totalPages)return}throw new CatalogFetchError(base,`Stopped after ${MAX_PAGES} pages of ${base}${API_PATH} without reaching the end.`)}export const wooCommerceAdapter={name:"woocommerce",pages:wooPages};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUIDv7}from"bun";import{db}from"@stacksjs/database/runtime";import{formatDate}from"@stacksjs/orm";import{normalizeStoreUrl,storeHost}from"./http";import{catalogUuid}from"./identity";export const UNKNOWN_COUNTRY="Unknown";const DEFAULT_PREPARATION_TIME=1,TYPE_MAX=50;export const SKU_MAX=100;export function lowestPrice(variants){const prices=variants.map((variant)=>variant.priceMinor).filter((price)=>price!==null);return prices.length>0?Math.min(...prices):null}export function totalInventory(variants){if(variants.length===0||variants.some((variant)=>variant.inventory===null))return null;return variants.reduce((sum,variant)=>sum+(variant.inventory??0),0)}export function variantSkus(product){const skus=new Map,warnings=[],claimed=new Set;for(const variant of product.variants){const sku=variant.sku;if(sku===null)skus.set(variant.externalId,null);else if(sku.length>SKU_MAX){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" has a SKU longer than ${SKU_MAX} characters; imported without it`})}else if(claimed.has(sku)){skus.set(variant.externalId,null);warnings.push({externalId:product.externalId,message:`"${product.name}" variant "${variant.title}" repeats SKU ${sku}; imported without it`})}else{claimed.add(sku);skus.set(variant.externalId,sku)}}return{skus,warnings}}export function productRow(product,host,refs,now,mode){const inventory=totalInventory(product.variants),row={uuid:catalogUuid(product.source,host,"product",product.externalId),name:product.name,description:product.descriptionHtml,price:lowestPrice(product.variants)??0,image_url:product.images[0]?.src??null,is_available:product.variants.some((variant)=>variant.available),updated_at:now};if(mode==="create"){row.inventory_count=inventory??0;row.preparation_time=DEFAULT_PREPARATION_TIME;row.category_id=refs.categoryId;row.manufacturer_id=refs.manufacturerId;row.created_at=now}else{if(inventory!==null)row.inventory_count=inventory;if(refs.categoryId!==null)row.category_id=refs.categoryId;if(refs.manufacturerId!==null)row.manufacturer_id=refs.manufacturerId}return row}export function variantRow(product,variant,host,productId,now,mode,sku=variant.sku){const row={uuid:catalogUuid(product.source,host,"variant",variant.externalId),product_id:productId,variant:variant.title,type:(product.optionNames.join(" / ")||"Option").slice(0,TYPE_MAX),options:JSON.stringify(variant.optionValues),status:variant.available?"active":"inactive",price:variant.priceMinor,compare_at_price:variant.compareAtMinor,updated_at:now};if(mode==="create"){row.sku=sku;row.inventory_count=variant.inventory;row.created_at=now}else{if(sku!==null)row.sku=sku;if(variant.inventory!==null)row.inventory_count=variant.inventory}return row}function withoutUuid(row){const changes={...row};delete changes.uuid;return changes}export async function importCatalog(options){const storeUrl=normalizeStoreUrl(options.storeUrl),host=storeHost(storeUrl),clock=options.now??(()=>new Date),{adapter,repository}=options,result={source:adapter.name,storeUrl,dryRun:options.dryRun===!0,pages:[],products:[],counts:{products:{created:0,updated:0},variants:{created:0,updated:0},categories:{created:0,updated:0},manufacturers:{created:0,updated:0},duplicates:0},currencies:[],warnings:[]},categoryIds=new Map,manufacturerIds=new Map,seenProducts=new Map,currencies=new Set;async function categoryFor(product,repo,now){const category=product.categories[0];if(!category)return null;const cached=categoryIds.get(category.slug);if(cached!==void 0)return cached;let id=await repo.findCategory(category.slug,category.name);if(id===void 0){id=await repo.createCategory({uuid:randomUUIDv7(),name:category.name,slug:category.slug,description:null,image_url:null,is_active:!0,parent_category_id:null,display_order:categoryIds.size,created_at:now,updated_at:now});result.counts.categories.created++}categoryIds.set(category.slug,id);return id}async function manufacturerFor(product,repo,now){if(!product.vendor)return null;const cached=manufacturerIds.get(product.vendor);if(cached!==void 0)return cached;let id=await repo.findManufacturer(product.vendor);if(id===void 0){id=await repo.createManufacturer({uuid:randomUUIDv7(),manufacturer:product.vendor,description:null,country:UNKNOWN_COUNTRY,featured:!1,created_at:now,updated_at:now});result.counts.manufacturers.created++}manufacturerIds.set(product.vendor,id);return id}async function existingFor(products){const productUuids=products.map((product)=>catalogUuid(product.source,host,"product",product.externalId)),variantUuids=products.filter((product)=>product.hasOptions).flatMap((product)=>product.variants.map((variant)=>catalogUuid(product.source,host,"variant",variant.externalId)));return{products:productUuids.length>0?await repository.findProducts(productUuids):new Map,variants:variantUuids.length>0?await repository.findVariants(variantUuids):new Map}}async function write(product,repo,known){const now=formatDate(clock()),refs={categoryId:await categoryFor(product,repo,now),manufacturerId:await manufacturerFor(product,repo,now)},uuid=catalogUuid(product.source,host,"product",product.externalId),existing=known.products.get(uuid),action=existing===void 0?"create":"update";let productId;if(existing===void 0)productId=await repo.createProduct(productRow(product,host,refs,now,"create"));else{productId=existing;await repo.updateProduct(productId,withoutUuid(productRow(product,host,refs,now,"update")))}const variants={create:0,update:0};if(product.hasOptions){const{skus,warnings}=variantSkus(product);result.warnings.push(...warnings);const claimed=[...skus.values()].filter((sku)=>sku!==null);if(existing!==void 0&&claimed.length>0)await repo.releaseVariantSkus(productId,claimed);for(const variant of product.variants){const sku=skus.get(variant.externalId)??null,found=known.variants.get(catalogUuid(product.source,host,"variant",variant.externalId));if(found===void 0){await repo.createVariant(variantRow(product,variant,host,productId,now,"create",sku));variants.create++}else{await repo.updateVariant(found,withoutUuid(variantRow(product,variant,host,productId,now,"update",sku)));variants.update++}}}return{action,externalId:product.externalId,name:product.name,handle:product.handle,priceMinor:lowestPrice(product.variants),currency:product.currency,variants,category:product.categories[0]?.name??null,vendor:product.vendor,imageCount:product.images.length}}for await(const page of adapter.pages(storeUrl,{limit:options.limit,fetch:options.fetch,currency:options.currency,accessKey:options.accessKey})){result.pages.push(page.url);result.warnings.push(...page.warnings);const fresh=[];for(const product of page.products){if(seenProducts.get(product.externalId)!==void 0){result.counts.duplicates++;result.warnings.push({externalId:product.externalId,message:`"${product.name}" appeared twice; imported once`});continue}seenProducts.set(product.externalId,product.handle);fresh.push(product)}const known=await existingFor(fresh);for(const product of fresh){if(product.currency)currencies.add(product.currency);const imported=repository.transaction?await repository.transaction((repo)=>write(product,repo,known)):await write(product,repository,known);result.counts.products[imported.action==="create"?"created":"updated"]++;result.counts.variants.created+=imported.variants.create;result.counts.variants.updated+=imported.variants.update;result.products.push(imported);options.onProduct?.(imported)}}result.currencies=[...currencies].sort();if(result.currencies.length>1)result.warnings.push({message:`prices arrived in more than one currency (${result.currencies.join(", ")}); products.price has no currency column`});return result}export function createDatabaseRepository(connection=db){async function idByUuid(table,uuid){const row=await connection.selectFrom(table).where("uuid","=",uuid).select("id").executeTakeFirst();if(!row)throw Error(`Inserted a row into ${table} but could not read it back (uuid ${uuid})`);return Number(row.id)}async function idsByUuid(table,uuids){const found=new Map;for(let start=0;start<uuids.length;start+=500){const rows=await connection.selectFrom(table).where("uuid","in",uuids.slice(start,start+500)).select(["id","uuid"]).execute();for(const row of rows)found.set(row.uuid,Number(row.id))}return found}async function first(query){const row=await query.select("id").executeTakeFirst();return row?Number(row.id):void 0}const repository={async findCategory(slug,name){return await first(connection.selectFrom("categories").where("slug","=",slug))??await first(connection.selectFrom("categories").where("name","=",name))},async createCategory(row){await connection.insertInto("categories").values(row).execute();return idByUuid("categories",row.uuid)},async findManufacturer(name){return first(connection.selectFrom("manufacturers").where("manufacturer","=",name))},async createManufacturer(row){await connection.insertInto("manufacturers").values(row).execute();return idByUuid("manufacturers",row.uuid)},async findProducts(uuids){return idsByUuid("products",uuids)},async createProduct(row){await connection.insertInto("products").values(row).execute();return idByUuid("products",row.uuid)},async updateProduct(id,row){await connection.updateTable("products").set(row).where("id","=",id).execute()},async findVariants(uuids){return idsByUuid("product_variants",uuids)},async createVariant(row){await connection.insertInto("product_variants").values(row).execute();return idByUuid("product_variants",row.uuid)},async updateVariant(id,row){await connection.updateTable("product_variants").set(row).where("id","=",id).execute()},async releaseVariantSkus(productId,skus){for(let start=0;start<skus.length;start+=500)await connection.updateTable("product_variants").set({sku:null}).where("product_id","=",productId).where("sku","in",skus.slice(start,start+500)).execute()}};if(connection===db)repository.transaction=(work)=>db.transaction((trx)=>work(createDatabaseRepository(trx)));return repository}export function createMemoryRepository(){const tables={categories:[],manufacturers:[],products:[],product_variants:[]};function insert(table,row){const id=tables[table].length+1;tables[table].push({id,...row});return id}function update(table,id,row){const target=tables[table].find((entry)=>entry.id===id);if(target)Object.assign(target,row)}const byUuid=(table,uuids)=>new Map(tables[table].filter((row)=>uuids.includes(row.uuid)).map((row)=>[row.uuid,row.id])),find=(table,match)=>tables[table].find(match)?.id;return{tables,findCategory:async(slug,name)=>find("categories",(row)=>row.slug===slug)??find("categories",(row)=>row.name===name),createCategory:async(row)=>insert("categories",row),findManufacturer:async(name)=>find("manufacturers",(row)=>row.manufacturer===name),createManufacturer:async(row)=>insert("manufacturers",row),findProducts:async(uuids)=>byUuid("products",uuids),createProduct:async(row)=>insert("products",row),updateProduct:async(id,row)=>update("products",id,row),findVariants:async(uuids)=>byUuid("product_variants",uuids),createVariant:async(row)=>insert("product_variants",row),updateVariant:async(id,row)=>update("product_variants",id,row),releaseVariantSkus:async(productId,skus)=>{for(const row of tables.product_variants)if(row.product_id===productId&&skus.includes(row.sku))row.sku=null}}}export function createReadOnlyRepository(inner){let nextId=-1;const fake=async()=>nextId--;return{findCategory:inner.findCategory,findManufacturer:inner.findManufacturer,findProducts:inner.findProducts,findVariants:inner.findVariants,createCategory:fake,createManufacturer:fake,createProduct:fake,createVariant:fake,updateProduct:async()=>{},updateVariant:async()=>{},releaseVariantSkus:async()=>{}}}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ import * as tax from './tax/index';
|
|
|
20
20
|
import * as waitlists from './waitlists/index';
|
|
21
21
|
export type { MoneyInputOptions } from './money';
|
|
22
22
|
export type { TaxRateRow } from './sales-tax';
|
|
23
|
+
export type { SalesTaxSetup, SalesTaxInputOptions } from './tax-input';
|
|
23
24
|
export declare const commerce: CommerceNamespace;
|
|
24
25
|
export declare interface CommerceNamespace {
|
|
25
26
|
auctions: AuctionsModule
|
|
@@ -102,4 +103,6 @@ export {
|
|
|
102
103
|
// Itemised sales tax on an amount: pure, so a register shows it before it
|
|
103
104
|
// charges. Browser code imports it from `@stacksjs/commerce/sales-tax`.
|
|
104
105
|
export { breakdownFor, multiplierOf } from './sales-tax';
|
|
106
|
+
export { restockedQuantity } from './inventory';
|
|
107
|
+
export { parseSalesTaxSetup, parseTaxRateWriteData } from './tax-input';
|
|
105
108
|
export default commerce;
|