Initial import of zappier-edge from zapier monorepo
This commit is contained in:
commit
78e6201d43
120 changed files with 19867 additions and 0 deletions
173
src/pricing.ts
Normal file
173
src/pricing.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
export type PriceRule =
|
||||
| { kind: 'free' }
|
||||
| { kind: 'fixed'; fixedCents: number }
|
||||
| { kind: 'variable'; baseCents: number; perKbCents: number; perMbCents: number };
|
||||
|
||||
export interface TierConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
multiplier: number;
|
||||
monthlyCreditCents: number;
|
||||
defaultRule?: PriceRule;
|
||||
}
|
||||
|
||||
export interface RateCard {
|
||||
endpoints: Record<string, PriceRule>;
|
||||
}
|
||||
|
||||
export interface TierCatalog {
|
||||
find(id: string): TierConfig | undefined;
|
||||
list(): TierConfig[];
|
||||
}
|
||||
|
||||
export class ConfigTierCatalog implements TierCatalog {
|
||||
constructor(private tiers: TierConfig[]) {}
|
||||
|
||||
find(id: string): TierConfig | undefined {
|
||||
return this.tiers.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
list(): TierConfig[] {
|
||||
return [...this.tiers];
|
||||
}
|
||||
}
|
||||
|
||||
export interface CallUsage {
|
||||
metadataBytes: number;
|
||||
attachmentBytes: number;
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
endpointId: string;
|
||||
listCents: number;
|
||||
totalCents: number;
|
||||
breakdown: { baseCents: number; metadataCents: number; attachmentCents: number };
|
||||
}
|
||||
|
||||
export interface PricingContext {
|
||||
rateCard: RateCard;
|
||||
tiers: TierCatalog;
|
||||
}
|
||||
|
||||
export interface PricingStore {
|
||||
getRateCard(): RateCard;
|
||||
getTiers(): TierConfig[];
|
||||
upsertEndpoint(endpointId: string, rule: PriceRule): void;
|
||||
deleteEndpoint(endpointId: string): void;
|
||||
upsertTier(tier: TierConfig): void;
|
||||
deleteTier(tierId: string): void;
|
||||
}
|
||||
|
||||
export const DEFAULT_RATE_CARD: RateCard = {
|
||||
endpoints: {
|
||||
status: { kind: 'free' },
|
||||
'storage-list': { kind: 'free' },
|
||||
transform: { kind: 'fixed', fixedCents: 4 },
|
||||
add: { kind: 'free' },
|
||||
timestamp: { kind: 'fixed', fixedCents: 4 },
|
||||
'hash-lookup': { kind: 'free' },
|
||||
receipt: { kind: 'free' },
|
||||
storage: { kind: 'variable', baseCents: 10, perKbCents: 1, perMbCents: 50 },
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_TIERS: TierConfig[] = [
|
||||
{ id: 'free', name: 'Free', multiplier: 1, monthlyCreditCents: 100 },
|
||||
{
|
||||
id: 'pro',
|
||||
name: 'Pro',
|
||||
multiplier: 0.5,
|
||||
monthlyCreditCents: 1000,
|
||||
defaultRule: { kind: 'fixed', fixedCents: 8 },
|
||||
},
|
||||
{
|
||||
id: 'business',
|
||||
name: 'Business',
|
||||
multiplier: 0.25,
|
||||
monthlyCreditCents: 10000,
|
||||
defaultRule: { kind: 'fixed', fixedCents: 8 },
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_PRICING: PricingContext = {
|
||||
rateCard: DEFAULT_RATE_CARD,
|
||||
tiers: new ConfigTierCatalog(DEFAULT_TIERS),
|
||||
};
|
||||
|
||||
export class InMemoryPricingStore implements PricingStore {
|
||||
private endpoints: Record<string, PriceRule>;
|
||||
private tiers: TierConfig[];
|
||||
|
||||
constructor(rateCard: RateCard = DEFAULT_RATE_CARD, tiers: TierConfig[] = DEFAULT_TIERS) {
|
||||
this.endpoints = { ...rateCard.endpoints };
|
||||
this.tiers = [...tiers];
|
||||
}
|
||||
|
||||
getRateCard(): RateCard {
|
||||
return { endpoints: { ...this.endpoints } };
|
||||
}
|
||||
|
||||
getTiers(): TierConfig[] {
|
||||
return [...this.tiers];
|
||||
}
|
||||
|
||||
upsertEndpoint(endpointId: string, rule: PriceRule): void {
|
||||
this.endpoints[endpointId] = rule;
|
||||
}
|
||||
|
||||
deleteEndpoint(endpointId: string): void {
|
||||
delete this.endpoints[endpointId];
|
||||
}
|
||||
|
||||
upsertTier(tier: TierConfig): void {
|
||||
const i = this.tiers.findIndex((t) => t.id === tier.id);
|
||||
if (i >= 0) this.tiers[i] = tier;
|
||||
else this.tiers.push(tier);
|
||||
}
|
||||
|
||||
deleteTier(tierId: string): void {
|
||||
this.tiers = this.tiers.filter((t) => t.id !== tierId);
|
||||
}
|
||||
}
|
||||
|
||||
export function quoteCall(
|
||||
pricing: PricingContext,
|
||||
tierId: string,
|
||||
endpointId: string,
|
||||
usage: CallUsage,
|
||||
multiplierOverride?: number,
|
||||
): Quote {
|
||||
const tier = pricing.tiers.find(tierId);
|
||||
if (!tier) throw new Error(`Unknown tier: ${tierId}`);
|
||||
const rule = pricing.rateCard.endpoints[endpointId] ?? tier.defaultRule;
|
||||
if (!rule) throw new Error(`No price rule for ${tierId}/${endpointId}`);
|
||||
|
||||
if (rule.kind === 'free') {
|
||||
return {
|
||||
endpointId,
|
||||
listCents: 0,
|
||||
totalCents: 0,
|
||||
breakdown: { baseCents: 0, metadataCents: 0, attachmentCents: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
let breakdown: Quote['breakdown'];
|
||||
if (rule.kind === 'fixed') {
|
||||
breakdown = { baseCents: rule.fixedCents, metadataCents: 0, attachmentCents: 0 };
|
||||
} else {
|
||||
breakdown = {
|
||||
baseCents: rule.baseCents,
|
||||
metadataCents: Math.ceil(usage.metadataBytes / 1024) * rule.perKbCents,
|
||||
attachmentCents:
|
||||
Math.ceil(usage.attachmentBytes / (1024 * 1024)) * rule.perMbCents,
|
||||
};
|
||||
}
|
||||
const listCents = breakdown.baseCents + breakdown.metadataCents + breakdown.attachmentCents;
|
||||
const multiplier = multiplierOverride ?? tier.multiplier;
|
||||
return {
|
||||
endpointId,
|
||||
listCents,
|
||||
totalCents: Math.round(listCents * multiplier),
|
||||
breakdown,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue