datewise/app/services/fees.server.ts
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

104 lines
4.7 KiB
TypeScript

/**
* Charging delivery fees and peak surcharges on every Shopify plan.
*
* Carrier-calculated rates need Advanced/Plus (or the paid add-on), so instead the
* app manages one hidden "Delivery & handling" product tagged `datewise-fee`, with a
* variant per fee amount. The widget puts exactly one fee line in the cart that
* matches the quote; the checkout validation function and the order webhook both
* re-check it.
*/
import prisma from "../db.server";
import { FEE_TAG } from "./catalog.server";
type AdminGraphql = { graphql: (q: string, o?: { variables?: Record<string, unknown> }) => Promise<Response> };
async function gql<T>(admin: AdminGraphql, q: string, variables?: Record<string, unknown>): Promise<T> {
const r = await admin.graphql(q, { variables });
const j = (await r.json()) as { data: T; errors?: unknown };
if (j.errors) throw new Error(`Admin API: ${JSON.stringify(j.errors)}`);
return j.data;
}
const label = (cents: number) => (cents / 100).toFixed(2);
const numericId = (gid: string) => gid.split("/").pop()!;
export async function ensureFeeProduct(admin: AdminGraphql, shopId: string): Promise<string> {
const shop = await prisma.shop.findUniqueOrThrow({ where: { id: shopId } });
if (shop.feeProductId) {
const d = await gql<{ product: { id: string } | null }>(admin, `#graphql
query($id: ID!) { product(id: $id) { id } }`, { id: shop.feeProductId });
if (d.product) return d.product.id;
}
const created = await gql<{ productCreate: { product: { id: string } | null; userErrors: { message: string }[] } }>(
admin,
`#graphql
mutation DatewiseFeeProduct($product: ProductCreateInput!) {
productCreate(product: $product) { product { id } userErrors { message } }
}`,
{
product: {
title: "Delivery & handling",
handle: "datewise-delivery-fee",
tags: [FEE_TAG],
status: "ACTIVE",
productType: "Service",
descriptionHtml: "<p>Added automatically for scheduled delivery and pickup.</p>",
productOptions: [{ name: "Fee", values: [{ name: label(0) }] }],
seo: { title: "Delivery & handling" },
},
},
);
const id = created.productCreate.product?.id;
if (!id) throw new Error(created.productCreate.userErrors.map((e) => e.message).join("; "));
// Hide from storefront search/collections.
await gql(admin, `#graphql
mutation($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { userErrors { message } } }`, {
m: [{ ownerId: id, namespace: "seo", key: "hidden", type: "number_integer", value: "1" }],
});
// Must be on the Online Store channel to be added to carts.
const pubs = await gql<{ publications: { nodes: { id: string; catalog: { title: string } | null }[] } }>(admin, `#graphql
query { publications(first: 20) { nodes { id catalog { title } } } }`);
const online = pubs.publications.nodes.find((p) => /online store/i.test(p.catalog?.title ?? ""));
if (online)
await gql(admin, `#graphql
mutation($id: ID!, $input: [PublicationInput!]!) { publishablePublish(id: $id, input: $input) { userErrors { message } } }`, {
id,
input: [{ publicationId: online.id }],
});
await prisma.shop.update({ where: { id: shopId }, data: { feeProductId: id } });
return id;
}
/** Returns the numeric variant id for a fee amount, creating the variant when needed. */
export async function ensureFeeVariant(admin: AdminGraphql, shopId: string, cents: number): Promise<string | null> {
if (cents <= 0) return null;
const productId = await ensureFeeProduct(admin, shopId);
const d = await gql<{ product: { variants: { nodes: { id: string; price: string }[] } } }>(admin, `#graphql
query($id: ID!) { product(id: $id) { variants(first: 250) { nodes { id price } } } }`, { id: productId });
const found = d.product.variants.nodes.find((v) => Math.round(Number(v.price) * 100) === cents);
if (found) return numericId(found.id);
const c = await gql<{ productVariantsBulkCreate: { productVariants: { id: string }[]; userErrors: { message: string }[] } }>(
admin,
`#graphql
mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkCreate(productId: $productId, variants: $variants) {
productVariants { id } userErrors { message }
}
}`,
{
productId,
variants: [
{
price: label(cents),
optionValues: [{ optionName: "Fee", name: label(cents) }],
inventoryPolicy: "CONTINUE",
inventoryItem: { tracked: false, requiresShipping: false },
},
],
},
);
const v = c.productVariantsBulkCreate.productVariants[0];
if (!v) throw new Error(c.productVariantsBulkCreate.userErrors.map((e) => e.message).join("; "));
return numericId(v.id);
}