"use node"; import { action } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import { internal } from "./_generated/api"; import { validateAddressWithShippo, computeParcel, getShippingRatesFromShippo, selectBestRate, MAX_PARCEL_WEIGHT_G, } from "./model/shippo"; import type { ShippingRateResult } from "./model/checkout"; export const validateAddress = action({ args: { addressLine1: v.string(), additionalInformation: v.optional(v.string()), city: v.string(), postalCode: v.string(), country: v.string(), name: v.optional(v.string()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("You must be signed in to validate an address."); } return await validateAddressWithShippo({ addressLine1: args.addressLine1, additionalInformation: args.additionalInformation, city: args.city, postalCode: args.postalCode, country: args.country, name: args.name, }); }, }); export const getShippingRate = action({ args: { addressId: v.id("addresses"), sessionId: v.optional(v.string()), }, handler: async (ctx, args): Promise => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("You must be signed in to get shipping rates."); } const address = await ctx.runQuery(internal.checkout.getAddressById, { addressId: args.addressId, }); if (address.isValidated !== true) { console.warn( `Shipping rate requested for unvalidated address ${args.addressId}. ` + "The shipping address step should validate before proceeding.", ); } const userId = await ctx.runQuery(internal.checkout.getCurrentUserId); const cartResult = await ctx.runQuery( internal.checkout.validateCartInternal, { userId, sessionId: args.sessionId }, ); if (!cartResult) { throw new ConvexError("Your cart is empty."); } if (!cartResult.valid) { throw new ConvexError( "Your cart has issues that need to be resolved before checkout.", ); } const parcel = computeParcel(cartResult.items); const weightG = parseFloat(parcel.weight); if (weightG > MAX_PARCEL_WEIGHT_G) { const actualKg = (weightG / 1000).toFixed(1); const maxKg = MAX_PARCEL_WEIGHT_G / 1000; throw new ConvexError( `Your order weighs ${actualKg}kg, which exceeds our maximum shipping weight of ${maxKg}kg. ` + "Please remove some items or reduce quantities to proceed.", ); } const shippoAddress = { name: address.fullName, street1: address.addressLine1, street2: address.additionalInformation, city: address.city, zip: address.postalCode, country: address.country, phone: address.phone, }; const sourceAddressId = process.env.SHIPPO_SOURCE_ADDRESS_ID; if (!sourceAddressId) { throw new ConvexError( "Shipping configuration is incomplete (missing source address).", ); } const { shipmentObjectId, rates } = await getShippingRatesFromShippo({ sourceAddressId, destinationAddress: shippoAddress, parcels: [parcel], }); const { selected, alternatives } = selectBestRate(rates); const mapRate = (r: typeof selected) => ({ provider: r.provider, serviceName: r.servicelevelName, serviceToken: r.servicelevelToken, amount: parseFloat(r.amount), currency: r.currency, estimatedDays: r.estimatedDays, durationTerms: r.durationTerms, carrierAccount: r.carrierAccount, }); return { shipmentObjectId, selectedRate: mapRate(selected), alternativeRates: alternatives.map(mapRate), cartSubtotal: cartResult.subtotal, shippingTotal: parseFloat(selected.amount), orderTotal: cartResult.subtotal + parseFloat(selected.amount), }; }, });