latest
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
deploy-secrets:
|
||||
fly secrets import < .env
|
||||
|
||||
deploy:
|
||||
bun vite build && fly deploy
|
||||
|
||||
@@ -4741,7 +4741,7 @@
|
||||
"metric": "weight",
|
||||
"spread": 3,
|
||||
"step": 12,
|
||||
"ratio": "inverse"
|
||||
"ratio": "normal"
|
||||
}
|
||||
],
|
||||
"unit": "ms",
|
||||
@@ -5285,13 +5285,13 @@
|
||||
"metric": "weight",
|
||||
"spread": 3,
|
||||
"step": 12,
|
||||
"ratio": "inverse"
|
||||
"ratio": "normal"
|
||||
},
|
||||
{
|
||||
"metric": "age",
|
||||
"spread": 4,
|
||||
"step": 10,
|
||||
"ratio": "inverse"
|
||||
"ratio": "normal"
|
||||
}
|
||||
],
|
||||
"unit": "ms",
|
||||
|
||||
+53
-4
@@ -3,11 +3,11 @@ import { cors } from "@elysiajs/cors";
|
||||
import { LevelCalculator, Standards, type ActivityStandards } from "$lib/services/calculator/main";
|
||||
import type { ActivityPerformance, Player } from "$lib/services/calculator/util";
|
||||
import rawStandards from "$lib/data/standards.json" assert { type: "json" }
|
||||
import { Printful } from "../services/commerce/util/types";
|
||||
import { Printful, Webflow } from "../services/commerce/util/types";
|
||||
import WebflowService from "../services/commerce/webflow";
|
||||
import SyncService from "../services/commerce/sync";
|
||||
import PrintfulService from "../services/commerce/printful";
|
||||
import { FetchError } from "../services/commerce/util/misc";
|
||||
import { FetchError, getStateFromZip } from "../services/commerce/util/misc";
|
||||
|
||||
export const app = new Elysia({ prefix: "/api" })
|
||||
.use(cors({ origin: '*' }))
|
||||
@@ -16,10 +16,10 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
FetchError
|
||||
})
|
||||
|
||||
.onError(({ code, error }) => {
|
||||
.onError(async ({ code, error }) => {
|
||||
switch (code) {
|
||||
case "FetchError":
|
||||
console.error(error.message, error.payload);
|
||||
console.error(error.message, await error.parse());
|
||||
return error;
|
||||
}
|
||||
})
|
||||
@@ -75,6 +75,55 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
}
|
||||
case Printful.Webhook.Event.ProductDeleted: {
|
||||
await WebflowService.Products.remove(payload.data.sync_product.external_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.post("/webhook/webflow", async ({ request, body, set }) => {
|
||||
if (!WebflowService.Util.verifyWebflowSignature(request, body)) {
|
||||
set.status = 400;
|
||||
return "Invalid signature";
|
||||
}
|
||||
|
||||
const payload = body as Webflow.Webhook.EventPayload;
|
||||
|
||||
console.log(payload.triggerType);
|
||||
switch (payload.triggerType) {
|
||||
case Webflow.Webhook.Event.OrderCreated: {
|
||||
const webflowOrder = payload.payload;
|
||||
|
||||
console.log("Creating printful order...");
|
||||
await PrintfulService.Orders.create({
|
||||
external_id: webflowOrder.orderId,
|
||||
// TODO: derive from webflow
|
||||
shipping: "STANDARD",
|
||||
recipient: {
|
||||
name: webflowOrder.shippingAddress.addressee,
|
||||
address1: webflowOrder.shippingAddress.line1,
|
||||
address2: webflowOrder.shippingAddress.line2,
|
||||
city: webflowOrder.shippingAddress.city,
|
||||
state_code: getStateFromZip(webflowOrder.shippingAddress.postalCode),
|
||||
country_code: webflowOrder.shippingAddress.country,
|
||||
zip: webflowOrder.shippingAddress.postalCode
|
||||
},
|
||||
items: webflowOrder.purchasedItems.map(webflowOrderSku => ({
|
||||
external_variant_id: webflowOrderSku.variantId,
|
||||
quantity: webflowOrderSku.count
|
||||
}))
|
||||
});
|
||||
|
||||
// set webflow order to pending
|
||||
|
||||
console.log("Complete");
|
||||
|
||||
break;
|
||||
}
|
||||
case Webflow.Webhook.Event.OrderUpdated: {
|
||||
|
||||
// read printful order status
|
||||
|
||||
// set webflow order to fulfulled (if indeed printful order got confirmed)
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,11 +178,15 @@ export class LevelCalculator {
|
||||
}
|
||||
|
||||
export type StandardsConfig = {
|
||||
maxLevel: number,
|
||||
weightModifier: number,
|
||||
weightSkew: number,
|
||||
ageModifier: number,
|
||||
disableGeneration: boolean
|
||||
global: {
|
||||
maxLevel: number,
|
||||
},
|
||||
activity: Record<Activity, {
|
||||
weightModifier: number,
|
||||
weightSkew: number,
|
||||
ageModifier: number,
|
||||
disableGeneration: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
export class Standards {
|
||||
@@ -190,12 +194,23 @@ export class Standards {
|
||||
private activityStandards: ActivityStandards;
|
||||
|
||||
constructor(activityStandards: ActivityStandards, cfg?: Partial<StandardsConfig>) {
|
||||
const defaultActivitiesConfig = Object.fromEntries(
|
||||
Object.values(Activity).map(activity => [
|
||||
activity,
|
||||
{
|
||||
disableGeneration: false,
|
||||
weightModifier: 0.1,
|
||||
weightSkew: 0,
|
||||
ageModifier: 0.1,
|
||||
},
|
||||
])
|
||||
) as Record<Activity, StandardsConfig["activity"][Activity]>;
|
||||
|
||||
this.cfg = {
|
||||
maxLevel: cfg?.maxLevel ?? 100,
|
||||
weightModifier: cfg?.weightModifier ?? .1,
|
||||
weightSkew: cfg?.weightSkew ?? .1,
|
||||
ageModifier: cfg?.ageModifier ?? .1,
|
||||
disableGeneration: cfg?.disableGeneration ?? false
|
||||
global: {
|
||||
maxLevel: cfg?.global?.maxLevel ?? 100,
|
||||
},
|
||||
activity: Object.assign(defaultActivitiesConfig, cfg?.activity)
|
||||
};
|
||||
|
||||
// prepare data
|
||||
@@ -204,12 +219,12 @@ export class Standards {
|
||||
// expand/compress
|
||||
for (const standard of this.activityStandards[activity].standards) {
|
||||
const expandedLevels = this.expandLevels(standard.levels, 5);
|
||||
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.maxLevel);
|
||||
const compressedLevels = this.compressLevels(expandedLevels, this.cfg.global.maxLevel);
|
||||
standard.levels = compressedLevels;
|
||||
}
|
||||
|
||||
// generate data
|
||||
const allGenerators = this.cfg.disableGeneration ?
|
||||
const allGenerators = this.cfg.activity[activity].disableGeneration ?
|
||||
[] :
|
||||
this.activityStandards[activity].metadata.generators;
|
||||
|
||||
@@ -241,7 +256,7 @@ export class Standards {
|
||||
for (const lvl in referenceStandard.levels) {
|
||||
if (!referenceStandard.levels[lvl]) continue;
|
||||
|
||||
const scalingExponent = this.cfg.ageModifier;
|
||||
const scalingExponent = this.cfg.activity[activity].ageModifier;
|
||||
const coefficient = ageGenerator.ratio === "inverse" ?
|
||||
referenceAge / currAge :
|
||||
currAge / referenceAge;
|
||||
@@ -271,7 +286,7 @@ export class Standards {
|
||||
for (const age of this.agesFor(activity, gender)) {
|
||||
const referenceWeight = getAvgWeight(gender, age);
|
||||
|
||||
const skewRatio = this.cfg.weightSkew; // 0 = normal, 1 = max skew
|
||||
const skewRatio = this.cfg.activity[activity].weightSkew; // 0 = normal, 1 = max skew
|
||||
const minWeight = referenceWeight - (weightGenerator.step * weightGenerator.spread * (1 - skewRatio));
|
||||
const maxWeight = referenceWeight + (weightGenerator.step * weightGenerator.spread * (1 + skewRatio));
|
||||
|
||||
@@ -295,7 +310,7 @@ export class Standards {
|
||||
for (const lvl in referenceStandard.levels) {
|
||||
if (!referenceStandard.levels[lvl]) continue;
|
||||
|
||||
const scalingExponent = this.cfg.weightModifier;
|
||||
const scalingExponent = this.cfg.activity[activity].weightModifier;
|
||||
const coefficient = weightGenerator.ratio === "inverse" ?
|
||||
referenceWeight / currWeight :
|
||||
currWeight / referenceWeight;
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class PrintfulService {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to get Printful variant", res);
|
||||
throw new FetchError("Failed to get Printful variant", res);
|
||||
}
|
||||
|
||||
const payload: Printful.Products.MetaDataSingle<Printful.Products.SyncVariant> = await res.json();
|
||||
@@ -34,7 +34,7 @@ export default class PrintfulService {
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to get all Printful products", res);
|
||||
throw new FetchError("Failed to get all Printful products", res);
|
||||
}
|
||||
|
||||
const payload: Printful.Products.MetaDataMulti<Printful.Products.SyncProduct> = await res.json();
|
||||
@@ -63,7 +63,7 @@ export default class PrintfulService {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to get Printful product", res);
|
||||
throw new FetchError("Failed to get Printful product", res);
|
||||
}
|
||||
|
||||
const payload: Printful.Products.MetaDataSingle<Printful.Products.Product> = await res.json();
|
||||
@@ -81,7 +81,24 @@ export default class PrintfulService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to update Printful product", res);
|
||||
throw new FetchError("Failed to update Printful product", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Orders = class {
|
||||
static async create(printfulOrder: Printful.Orders.Order) {
|
||||
const res = await fetch(`${env().API_URL}/orders`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...env().AUTH_HEADERS
|
||||
},
|
||||
body: JSON.stringify(printfulOrder)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new FetchError("Failed to create printful order", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,200 @@ export class FetchError extends Error {
|
||||
this.name = "FetchError";
|
||||
}
|
||||
|
||||
static async createAndParse(message: string, response: Response): Promise<FetchError> {
|
||||
const fetchError = new FetchError(message, response);
|
||||
async parse() {
|
||||
try {
|
||||
fetchError.payload = await fetchError.response.clone().json();
|
||||
this.payload = await this.response.clone().json();
|
||||
} catch {
|
||||
fetchError.payload = await fetchError.response.text().catch(() => undefined);
|
||||
this.payload = await this.response.text().catch(() => undefined);
|
||||
}
|
||||
finally {
|
||||
return fetchError;
|
||||
return this.payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStateFromZip(zipString: string): string {
|
||||
|
||||
/* Ensure param is a string to prevent unpredictable parsing results */
|
||||
if (typeof zipString !== 'string') {
|
||||
console.error('Must pass the zipcode as a string.');
|
||||
return "none";
|
||||
}
|
||||
|
||||
/* Ensure we have exactly 5 characters to parse */
|
||||
if (zipString.length !== 5) {
|
||||
console.error('Must pass a 5-digit zipcode.');
|
||||
return "none";
|
||||
}
|
||||
|
||||
/* Ensure we don't parse strings starting with 0 as octal values */
|
||||
const zipcode = parseInt(zipString, 10);
|
||||
|
||||
let st;
|
||||
let state;
|
||||
|
||||
/* Code cases alphabetized by state */
|
||||
if (zipcode >= 35000 && zipcode <= 36999) {
|
||||
st = 'AL';
|
||||
state = 'Alabama';
|
||||
} else if (zipcode >= 99500 && zipcode <= 99999) {
|
||||
st = 'AK';
|
||||
state = 'Alaska';
|
||||
} else if (zipcode >= 85000 && zipcode <= 86999) {
|
||||
st = 'AZ';
|
||||
state = 'Arizona';
|
||||
} else if (zipcode >= 71600 && zipcode <= 72999) {
|
||||
st = 'AR';
|
||||
state = 'Arkansas';
|
||||
} else if (zipcode >= 90000 && zipcode <= 96699) {
|
||||
st = 'CA';
|
||||
state = 'California';
|
||||
} else if (zipcode >= 80000 && zipcode <= 81999) {
|
||||
st = 'CO';
|
||||
state = 'Colorado';
|
||||
} else if ((zipcode >= 6000 && zipcode <= 6389) || (zipcode >= 6391 && zipcode <= 6999)) {
|
||||
st = 'CT';
|
||||
state = 'Connecticut';
|
||||
} else if (zipcode >= 19700 && zipcode <= 19999) {
|
||||
st = 'DE';
|
||||
state = 'Delaware';
|
||||
} else if (zipcode >= 32000 && zipcode <= 34999) {
|
||||
st = 'FL';
|
||||
state = 'Florida';
|
||||
} else if ((zipcode >= 30000 && zipcode <= 31999) || (zipcode >= 39800 && zipcode <= 39999)) {
|
||||
st = 'GA';
|
||||
state = 'Georgia';
|
||||
} else if (zipcode >= 96700 && zipcode <= 96999) {
|
||||
st = 'HI';
|
||||
state = 'Hawaii';
|
||||
} else if (zipcode >= 83200 && zipcode <= 83999 && zipcode != 83414) {
|
||||
st = 'ID';
|
||||
state = 'Idaho';
|
||||
} else if (zipcode >= 60000 && zipcode <= 62999) {
|
||||
st = 'IL';
|
||||
state = 'Illinois';
|
||||
} else if (zipcode >= 46000 && zipcode <= 47999) {
|
||||
st = 'IN';
|
||||
state = 'Indiana';
|
||||
} else if (zipcode >= 50000 && zipcode <= 52999) {
|
||||
st = 'IA';
|
||||
state = 'Iowa';
|
||||
} else if (zipcode >= 66000 && zipcode <= 67999) {
|
||||
st = 'KS';
|
||||
state = 'Kansas';
|
||||
} else if (zipcode >= 40000 && zipcode <= 42999) {
|
||||
st = 'KY';
|
||||
state = 'Kentucky';
|
||||
} else if (zipcode >= 70000 && zipcode <= 71599) {
|
||||
st = 'LA';
|
||||
state = 'Louisiana';
|
||||
} else if (zipcode >= 3900 && zipcode <= 4999) {
|
||||
st = 'ME';
|
||||
state = 'Maine';
|
||||
} else if (zipcode >= 20600 && zipcode <= 21999) {
|
||||
st = 'MD';
|
||||
state = 'Maryland';
|
||||
} else if ((zipcode >= 1000 && zipcode <= 2799) || (zipcode == 5501) || (zipcode == 5544)) {
|
||||
st = 'MA';
|
||||
state = 'Massachusetts';
|
||||
} else if (zipcode >= 48000 && zipcode <= 49999) {
|
||||
st = 'MI';
|
||||
state = 'Michigan';
|
||||
} else if (zipcode >= 55000 && zipcode <= 56899) {
|
||||
st = 'MN';
|
||||
state = 'Minnesota';
|
||||
} else if (zipcode >= 38600 && zipcode <= 39999) {
|
||||
st = 'MS';
|
||||
state = 'Mississippi';
|
||||
} else if (zipcode >= 63000 && zipcode <= 65999) {
|
||||
st = 'MO';
|
||||
state = 'Missouri';
|
||||
} else if (zipcode >= 59000 && zipcode <= 59999) {
|
||||
st = 'MT';
|
||||
state = 'Montana';
|
||||
} else if (zipcode >= 27000 && zipcode <= 28999) {
|
||||
st = 'NC';
|
||||
state = 'North Carolina';
|
||||
} else if (zipcode >= 58000 && zipcode <= 58999) {
|
||||
st = 'ND';
|
||||
state = 'North Dakota';
|
||||
} else if (zipcode >= 68000 && zipcode <= 69999) {
|
||||
st = 'NE';
|
||||
state = 'Nebraska';
|
||||
} else if (zipcode >= 88900 && zipcode <= 89999) {
|
||||
st = 'NV';
|
||||
state = 'Nevada';
|
||||
} else if (zipcode >= 3000 && zipcode <= 3899) {
|
||||
st = 'NH';
|
||||
state = 'New Hampshire';
|
||||
} else if (zipcode >= 7000 && zipcode <= 8999) {
|
||||
st = 'NJ';
|
||||
state = 'New Jersey';
|
||||
} else if (zipcode >= 87000 && zipcode <= 88499) {
|
||||
st = 'NM';
|
||||
state = 'New Mexico';
|
||||
} else if ((zipcode >= 10000 && zipcode <= 14999) || (zipcode == 6390) || (zipcode == 501) || (zipcode == 544)) {
|
||||
st = 'NY';
|
||||
state = 'New York';
|
||||
} else if (zipcode >= 43000 && zipcode <= 45999) {
|
||||
st = 'OH';
|
||||
state = 'Ohio';
|
||||
} else if ((zipcode >= 73000 && zipcode <= 73199) || (zipcode >= 73400 && zipcode <= 74999)) {
|
||||
st = 'OK';
|
||||
state = 'Oklahoma';
|
||||
} else if (zipcode >= 97000 && zipcode <= 97999) {
|
||||
st = 'OR';
|
||||
state = 'Oregon';
|
||||
} else if (zipcode >= 15000 && zipcode <= 19699) {
|
||||
st = 'PA';
|
||||
state = 'Pennsylvania';
|
||||
} else if (zipcode >= 300 && zipcode <= 999) {
|
||||
st = 'PR';
|
||||
state = 'Puerto Rico';
|
||||
} else if (zipcode >= 2800 && zipcode <= 2999) {
|
||||
st = 'RI';
|
||||
state = 'Rhode Island';
|
||||
} else if (zipcode >= 29000 && zipcode <= 29999) {
|
||||
st = 'SC';
|
||||
state = 'South Carolina';
|
||||
} else if (zipcode >= 57000 && zipcode <= 57999) {
|
||||
st = 'SD';
|
||||
state = 'South Dakota';
|
||||
} else if (zipcode >= 37000 && zipcode <= 38599) {
|
||||
st = 'TN';
|
||||
state = 'Tennessee';
|
||||
} else if ((zipcode >= 75000 && zipcode <= 79999) || (zipcode >= 73301 && zipcode <= 73399) || (zipcode >= 88500 && zipcode <= 88599)) {
|
||||
st = 'TX';
|
||||
state = 'Texas';
|
||||
} else if (zipcode >= 84000 && zipcode <= 84999) {
|
||||
st = 'UT';
|
||||
state = 'Utah';
|
||||
} else if (zipcode >= 5000 && zipcode <= 5999) {
|
||||
st = 'VT';
|
||||
state = 'Vermont';
|
||||
} else if ((zipcode >= 20100 && zipcode <= 20199) || (zipcode >= 22000 && zipcode <= 24699) || (zipcode == 20598)) {
|
||||
st = 'VA';
|
||||
state = 'Virginia';
|
||||
} else if ((zipcode >= 20000 && zipcode <= 20099) || (zipcode >= 20200 && zipcode <= 20599) || (zipcode >= 56900 && zipcode <= 56999)) {
|
||||
st = 'DC';
|
||||
state = 'Washington DC';
|
||||
} else if (zipcode >= 98000 && zipcode <= 99499) {
|
||||
st = 'WA';
|
||||
state = 'Washington';
|
||||
} else if (zipcode >= 24700 && zipcode <= 26999) {
|
||||
st = 'WV';
|
||||
state = 'West Virginia';
|
||||
} else if (zipcode >= 53000 && zipcode <= 54999) {
|
||||
st = 'WI';
|
||||
state = 'Wisconsin';
|
||||
} else if ((zipcode >= 82000 && zipcode <= 83199) || zipcode == 83414) {
|
||||
st = 'WY';
|
||||
state = 'Wyoming';
|
||||
} else {
|
||||
st = 'none';
|
||||
state = 'none';
|
||||
}
|
||||
|
||||
/* Return `state` for full name or `st` for postal abbreviation */
|
||||
return st;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DeepPartial } from "./misc";
|
||||
|
||||
export namespace Printful {
|
||||
export namespace Products {
|
||||
export interface MetaDataSingle<T = any> {
|
||||
@@ -84,12 +86,37 @@ export namespace Printful {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Orders {
|
||||
export type Order = {
|
||||
external_id: string,
|
||||
shipping: string
|
||||
recipient: {
|
||||
name: string
|
||||
company?: string
|
||||
address1: string
|
||||
address2: string
|
||||
city: string
|
||||
state_code: string
|
||||
state_name?: string
|
||||
country_code: string
|
||||
country_name?: string
|
||||
zip: string
|
||||
phone?: string
|
||||
email?: string
|
||||
tax_number?: string
|
||||
}
|
||||
items: Array<{ external_variant_id: string, quantity: number }>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Webhook {
|
||||
// meta
|
||||
interface MetaData {
|
||||
interface MetaData<E extends Event, D> {
|
||||
type: E;
|
||||
created: number;
|
||||
retries: number;
|
||||
store: number;
|
||||
data: D
|
||||
}
|
||||
|
||||
export enum Event {
|
||||
@@ -98,32 +125,26 @@ export namespace Printful {
|
||||
}
|
||||
|
||||
// product updated
|
||||
export interface ProductUpdated extends MetaData {
|
||||
type: Event.ProductUpdated,
|
||||
data: {
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
export interface ProductUpdated extends MetaData<Event.ProductUpdated, {
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
variants: number;
|
||||
synced: number;
|
||||
thumbnail_url: string;
|
||||
is_ignored: boolean;
|
||||
};
|
||||
}> { }
|
||||
|
||||
// product deleted
|
||||
export interface ProductDeleted extends MetaData {
|
||||
type: Event.ProductDeleted,
|
||||
data: {
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
export interface ProductDeleted extends MetaData<Event.ProductDeleted, {
|
||||
sync_product: {
|
||||
id: number;
|
||||
external_id: string;
|
||||
name: string;
|
||||
};
|
||||
}> { }
|
||||
|
||||
export type EventPayload = ProductUpdated | ProductDeleted
|
||||
}
|
||||
@@ -185,4 +206,187 @@ export namespace Webflow {
|
||||
publishStatus?: "staging" | "live";
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Orders {
|
||||
export type Order = {
|
||||
orderId: string
|
||||
status: string
|
||||
comment: string
|
||||
orderComment: string
|
||||
acceptedOn: string
|
||||
fulfilledOn: string | null
|
||||
refundedOn: string | null
|
||||
disputedOn: string | null
|
||||
disputeUpdatedOn: string | null
|
||||
disputeLastStatus: string | null
|
||||
customerPaid: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
netAmount: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
applicationFee: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
allAddresses: Array<{
|
||||
type: string
|
||||
addressee: string
|
||||
line1: string
|
||||
line2: string
|
||||
city: string
|
||||
state: string
|
||||
country: string
|
||||
postalCode: string
|
||||
}>
|
||||
shippingAddress: {
|
||||
type: string
|
||||
japanType: string
|
||||
addressee: string
|
||||
line1: string
|
||||
line2: string
|
||||
city: string
|
||||
state: string
|
||||
country: string
|
||||
postalCode: string
|
||||
}
|
||||
billingAddress: {
|
||||
type: string
|
||||
addressee: string
|
||||
line1: string
|
||||
line2: string
|
||||
city: string
|
||||
state: string
|
||||
country: string
|
||||
postalCode: string
|
||||
}
|
||||
shippingProvider: string
|
||||
shippingTracking: string
|
||||
shippingTrackingURL: string
|
||||
customerInfo: {
|
||||
fullName: string
|
||||
email: string
|
||||
}
|
||||
purchasedItems: Array<{
|
||||
count: number
|
||||
rowTotal: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
productId: string
|
||||
productName: string
|
||||
productSlug: string
|
||||
variantId: string
|
||||
variantName: string
|
||||
variantSlug: string
|
||||
variantSKU: string
|
||||
variantImage: {
|
||||
url: string
|
||||
file: {
|
||||
size: number;
|
||||
originalFileName: string;
|
||||
createdOn: string;
|
||||
contentType: string;
|
||||
width: number;
|
||||
height: number;
|
||||
variants: {
|
||||
size: number;
|
||||
url: string,
|
||||
originalFileName: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[]
|
||||
}
|
||||
}
|
||||
variantPrice: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
weight: number
|
||||
width: number
|
||||
height: number
|
||||
length: number
|
||||
}>
|
||||
purchasedItemsCount: number
|
||||
stripeDetails: {
|
||||
subscriptionId: string | null
|
||||
paymentMethod: string
|
||||
paymentIntentId: string
|
||||
customerId: string
|
||||
chargeId: string
|
||||
disputeId: string | null
|
||||
refundId: string
|
||||
refundReason: string
|
||||
}
|
||||
stripeCard: {
|
||||
last4: string
|
||||
brand: string
|
||||
ownerName: string
|
||||
expires: {
|
||||
year: number
|
||||
month: number
|
||||
}
|
||||
}
|
||||
paypalDetails: Object
|
||||
customData: Array<Object>
|
||||
metadata: {
|
||||
isBuyNow: boolean
|
||||
hasDownloads: boolean
|
||||
paymentProcessor: string
|
||||
}
|
||||
isCustomerDeleted: boolean
|
||||
isShippingRequired: boolean
|
||||
totals: {
|
||||
subtotal: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
extras: Array<{
|
||||
type: string
|
||||
name: string
|
||||
description: string
|
||||
price: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
}>
|
||||
total: {
|
||||
unit: string
|
||||
value: string
|
||||
string: string
|
||||
}
|
||||
}
|
||||
downloadFiles: Array<{
|
||||
id: string
|
||||
name: string
|
||||
url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Webhook {
|
||||
interface MetaData<E extends Event, D> {
|
||||
triggerType: E;
|
||||
payload: D
|
||||
}
|
||||
|
||||
export enum Event {
|
||||
OrderCreated = "ecomm_new_order",
|
||||
OrderUpdated = "ecomm_order_changed"
|
||||
}
|
||||
|
||||
export interface OrderCreated extends MetaData<Event.OrderCreated, Orders.Order> { }
|
||||
export interface OrderUpdated extends MetaData<Event.OrderUpdated, Orders.Order> { }
|
||||
|
||||
export type EventPayload = OrderCreated | OrderUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Webflow } from "./util/types";
|
||||
import { FetchError, type DeepPartial } from "./util/misc";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export default class WebflowService {
|
||||
static Products = class {
|
||||
@@ -17,7 +18,7 @@ export default class WebflowService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to create Webflow product SKU", res);
|
||||
throw new FetchError("Failed to create Webflow product SKU", res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ export default class WebflowService {
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to update Webflow product SKU", res);
|
||||
throw new FetchError("Failed to update Webflow product SKU", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +50,7 @@ export default class WebflowService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to create Webflow product", res);
|
||||
throw new FetchError("Failed to create Webflow product", res);
|
||||
}
|
||||
|
||||
const createdWebflowProduct = await res.json();
|
||||
@@ -65,7 +66,7 @@ export default class WebflowService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to get all Webflow products", res);
|
||||
throw new FetchError("Failed to get all Webflow products", res);
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
@@ -85,7 +86,7 @@ export default class WebflowService {
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to get Webflow product", res);
|
||||
throw new FetchError("Failed to get Webflow product", res);
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
@@ -103,7 +104,7 @@ export default class WebflowService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to update Webflow product", res);
|
||||
throw new FetchError("Failed to update Webflow product", res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +119,56 @@ export default class WebflowService {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await FetchError.createAndParse("Failed to remove Webflow product", res);
|
||||
throw new FetchError("Failed to remove Webflow product", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Util = class {
|
||||
static verifyWebflowSignature(request: Request, body: unknown) {
|
||||
try {
|
||||
const timestamp = request.headers.get("x-webflow-timestamp");
|
||||
if (!timestamp) {
|
||||
throw new Error("No timestamp provided");
|
||||
}
|
||||
const providedSignature = request.headers.get("x-webflow-signature");
|
||||
if (!providedSignature) {
|
||||
throw new Error("No signature provided");
|
||||
}
|
||||
|
||||
const secret = env().WEBHOOK_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("No secret provided");
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
throw new Error("Body is empty");
|
||||
}
|
||||
|
||||
const requestTimestamp = parseInt(timestamp, 10);
|
||||
const data = `${requestTimestamp}:${JSON.stringify(body)}`;
|
||||
const hash = crypto.createHmac('sha256', secret)
|
||||
.update(data)
|
||||
.digest('hex');
|
||||
|
||||
if (!crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(providedSignature, 'hex'))) {
|
||||
throw new Error('Invalid signature');
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - requestTimestamp > 300000) {
|
||||
throw new Error('Request is older than 5 minutes');
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error verifying signature: ${err}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const env = () => {
|
||||
@@ -133,6 +180,7 @@ const env = () => {
|
||||
SITE_ID: Bun.env.WEBFLOW_SITE_ID,
|
||||
COLLECTIONS_ID: Bun.env.WEBFLOW_COLLECTION_ID,
|
||||
AUTH_TOKEN: Bun.env.WEBFLOW_AUTH,
|
||||
WEBHOOK_SECRET: Bun.env.WEBFLOW_WEBHOOK_SECRET
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -20,11 +20,20 @@
|
||||
weight: "",
|
||||
},
|
||||
cfg: {
|
||||
enableGeneration: true,
|
||||
maxLevel: "100",
|
||||
weightModfier: ".1",
|
||||
weightSkew: ".1",
|
||||
ageModifier: ".1",
|
||||
global: {
|
||||
maxLevel: "100",
|
||||
},
|
||||
activity: Object.fromEntries(
|
||||
Object.values(Activity).map((activity) => [
|
||||
activity,
|
||||
{
|
||||
enableGeneration: true,
|
||||
weightModfier: ".1",
|
||||
weightSkew: "0",
|
||||
ageModifier: ".1",
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,11 +45,22 @@
|
||||
weight: lbToKg(+input.metrics.weight),
|
||||
} as Metrics,
|
||||
cfg: {
|
||||
maxLevel: +input.cfg.maxLevel,
|
||||
weightModifier: +input.cfg.weightModfier,
|
||||
weightSkew: +input.cfg.weightSkew,
|
||||
ageModifier: +input.cfg.ageModifier,
|
||||
disableGeneration: !input.cfg.enableGeneration,
|
||||
global: {
|
||||
maxLevel: +input.cfg.global.maxLevel,
|
||||
},
|
||||
activity: Object.fromEntries(
|
||||
Object.values(Activity).map((activity) => [
|
||||
activity,
|
||||
{
|
||||
weightModifier:
|
||||
+input.cfg.activity[activity].weightModfier,
|
||||
weightSkew: +input.cfg.activity[activity].weightSkew,
|
||||
ageModifier: +input.cfg.activity[activity].ageModifier,
|
||||
disableGeneration:
|
||||
!input.cfg.activity[activity].enableGeneration,
|
||||
},
|
||||
]),
|
||||
),
|
||||
} as StandardsConfig,
|
||||
});
|
||||
|
||||
@@ -82,7 +102,7 @@
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="5"
|
||||
bind:value={input.cfg.maxLevel}
|
||||
bind:value={input.cfg.global.maxLevel}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -97,7 +117,10 @@
|
||||
<label class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={input.cfg.enableGeneration}
|
||||
bind:checked={
|
||||
input.cfg.activity[selected.activity]
|
||||
.enableGeneration
|
||||
}
|
||||
class="toggle toggle-lg m-auto"
|
||||
/>
|
||||
</label>
|
||||
@@ -110,8 +133,12 @@
|
||||
max="1"
|
||||
step=".05"
|
||||
placeholder=".1"
|
||||
bind:value={input.cfg.weightModfier}
|
||||
disabled={selected.cfg.disableGeneration}
|
||||
bind:value={
|
||||
input.cfg.activity[selected.activity]
|
||||
.weightModfier
|
||||
}
|
||||
disabled={selected.cfg.activity[selected.activity]
|
||||
.disableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@@ -123,8 +150,11 @@
|
||||
max="1"
|
||||
step=".05"
|
||||
placeholder=".1"
|
||||
bind:value={input.cfg.weightSkew}
|
||||
disabled={selected.cfg.disableGeneration}
|
||||
bind:value={
|
||||
input.cfg.activity[selected.activity].weightSkew
|
||||
}
|
||||
disabled={selected.cfg.activity[selected.activity]
|
||||
.disableGeneration}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@@ -136,8 +166,12 @@
|
||||
max="1"
|
||||
step=".05"
|
||||
placeholder=".1"
|
||||
bind:value={input.cfg.ageModifier}
|
||||
disabled={selected.cfg.disableGeneration}
|
||||
bind:value={
|
||||
input.cfg.activity[selected.activity]
|
||||
.ageModifier
|
||||
}
|
||||
disabled={selected.cfg.activity[selected.activity]
|
||||
.disableGeneration}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user