fix: react-doctor unused-export 141→~80 (remove dead exports, enums/marketing unused, inline campaignStatusEnum)
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Shared enums for the SaaS schema. Mirrored in SQL as TEXT + CHECK.
|
||||
*
|
||||
* Usage:
|
||||
* import { tenantStatusEnum, type TenantStatus } from "@/db/schema/enums";
|
||||
* import { pgEnum } from "drizzle-orm/pg-core";
|
||||
*
|
||||
* export const tenantStatus = pgEnum("tenant_status", tenantStatusEnum);
|
||||
*/
|
||||
|
||||
export const tenantStatusEnum = [
|
||||
"trial",
|
||||
"active",
|
||||
"past_due",
|
||||
"suspended",
|
||||
"churned",
|
||||
] as const;
|
||||
export type TenantStatus = (typeof tenantStatusEnum)[number];
|
||||
|
||||
export const authProviderEnum = ["dev", "google", "email"] as const;
|
||||
export type AuthProvider = (typeof authProviderEnum)[number];
|
||||
|
||||
export const roleEnum = ["platform_admin", "brand_admin", "store_employee"] as const;
|
||||
export type Role = (typeof roleEnum)[number];
|
||||
|
||||
export const planCodeEnum = ["starter", "farm", "enterprise"] as const;
|
||||
export type PlanCode = (typeof planCodeEnum)[number];
|
||||
|
||||
export const addOnCodeEnum = [
|
||||
"wholesale_portal",
|
||||
"harvest_reach",
|
||||
"ai_tools",
|
||||
"water_log",
|
||||
"square_sync",
|
||||
"sms_campaigns",
|
||||
] as const;
|
||||
export type AddOnCode = (typeof addOnCodeEnum)[number];
|
||||
|
||||
export const subscriptionStatusEnum = [
|
||||
"trialing",
|
||||
"active",
|
||||
"past_due",
|
||||
"canceled",
|
||||
"incomplete",
|
||||
] as const;
|
||||
export type SubscriptionStatus = (typeof subscriptionStatusEnum)[number];
|
||||
|
||||
export const addOnStatusEnum = ["active", "canceled"] as const;
|
||||
export type AddOnStatus = (typeof addOnStatusEnum)[number];
|
||||
|
||||
export const stopStatusEnum = ["active", "paused", "closed"] as const;
|
||||
export type StopStatus = (typeof stopStatusEnum)[number];
|
||||
|
||||
export const orderStatusEnum = [
|
||||
"pending",
|
||||
"confirmed",
|
||||
"fulfilled",
|
||||
"canceled",
|
||||
] as const;
|
||||
export type OrderStatus = (typeof orderStatusEnum)[number];
|
||||
|
||||
export const fulfillmentEnum = ["pickup", "ship", "mixed"] as const;
|
||||
export type Fulfillment = (typeof fulfillmentEnum)[number];
|
||||
|
||||
export const itemFulfillmentEnum = ["pickup", "ship"] as const;
|
||||
export type ItemFulfillment = (typeof itemFulfillmentEnum)[number];
|
||||
|
||||
export const campaignStatusEnum = [
|
||||
"draft",
|
||||
"scheduled",
|
||||
"sending",
|
||||
"sent",
|
||||
"canceled",
|
||||
] as const;
|
||||
export type CampaignStatus = (typeof campaignStatusEnum)[number];
|
||||
@@ -10,9 +10,16 @@ import {
|
||||
timestamp,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { campaignStatusEnum } from "./enums";
|
||||
import { brands } from "./brands";
|
||||
|
||||
const campaignStatusEnum = [
|
||||
"draft",
|
||||
"scheduled",
|
||||
"sending",
|
||||
"sent",
|
||||
"canceled",
|
||||
] as const;
|
||||
|
||||
export const emailTemplates = pgTable(
|
||||
"email_templates",
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ const MIN_PASSWORD_LENGTH = 8;
|
||||
* Use this for admin-initiated password resets. For self-service
|
||||
* password changes, users should use the forgot password flow.
|
||||
*/
|
||||
export async function updatePasswordAction(
|
||||
async function updatePasswordAction(
|
||||
userId: string,
|
||||
newPassword: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
@@ -28,7 +28,7 @@ function getPriceId(key: string): string | null {
|
||||
|
||||
// ── Checkout session creation ─────────────────────────────────────────────────
|
||||
|
||||
export async function createStripeCheckoutSession(
|
||||
async function createStripeCheckoutSession(
|
||||
brandId: string,
|
||||
priceKey: string,
|
||||
successPath: string,
|
||||
|
||||
@@ -196,7 +196,7 @@ export type UpsertContactResult = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function upsertContact(contact: {
|
||||
async function upsertContact(contact: {
|
||||
id?: string;
|
||||
brand_id: string;
|
||||
email?: string;
|
||||
@@ -384,7 +384,7 @@ export type OptOutResult = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function optOutContact(params: {
|
||||
async function optOutContact(params: {
|
||||
email: string;
|
||||
brandId: string;
|
||||
method: "email" | "sms";
|
||||
|
||||
@@ -237,7 +237,7 @@ await getSession();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDashboardSummary(): Promise<DashboardSummary> {
|
||||
async function getDashboardSummary(): Promise<DashboardSummary> {
|
||||
|
||||
await getSession();
|
||||
try {
|
||||
|
||||
@@ -251,7 +251,7 @@ await getSession();
|
||||
|
||||
// ── Mark cart as recovered when order is placed ────────────────────────────────
|
||||
|
||||
export async function markCartRecovered(cartId: string, orderId: string): Promise<void> {
|
||||
async function markCartRecovered(cartId: string, orderId: string): Promise<void> {
|
||||
|
||||
await getSession(); void cartId;
|
||||
void orderId;
|
||||
|
||||
@@ -181,7 +181,7 @@ function buildWelcomeEmail(params: {
|
||||
|
||||
// ── Send one welcome email ─────────────────────────────────────────────────────
|
||||
|
||||
export async function sendWelcomeEmail(
|
||||
async function sendWelcomeEmail(
|
||||
entry: WelcomeSequenceEntry,
|
||||
step: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
@@ -75,7 +75,7 @@ function rowToCampaign(row: typeof campaigns.$inferSelect): Campaign {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHarvestReachCampaigns(
|
||||
async function getHarvestReachCampaigns(
|
||||
brandId: string
|
||||
): Promise<{ success: true; campaigns: Campaign[] } | { success: false; error: string }> {
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ await getSession(); const settings = await getAIProviderSettings(brandId);
|
||||
|
||||
// ── Custom integrations ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function getCustomIntegrations(brandId: string): Promise<CustomIntegration[]> {
|
||||
async function getCustomIntegrations(brandId: string): Promise<CustomIntegration[]> {
|
||||
|
||||
await getSession(); try {
|
||||
const res = await pool.query<CustomIntegration>(
|
||||
@@ -209,7 +209,7 @@ await getSession(); try {
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertCustomIntegration(
|
||||
async function upsertCustomIntegration(
|
||||
brandId: string,
|
||||
integration: CustomIntegration
|
||||
): Promise<{ success: boolean; integrations?: CustomIntegration[]; error?: string }> {
|
||||
@@ -230,7 +230,7 @@ await getSession(); const adminUser = await getAdminUser();
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCustomIntegration(
|
||||
async function deleteCustomIntegration(
|
||||
brandId: string,
|
||||
integrationId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
@@ -46,7 +46,7 @@ interface ClientAction {
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function listClientActions(): Promise<ClientAction[]> {
|
||||
async function listClientActions(): Promise<ClientAction[]> {
|
||||
|
||||
await getSession(); // Wire real server actions here as they land:
|
||||
// { name: "markOrderReady", handler: markOrderReady },
|
||||
|
||||
@@ -136,21 +136,21 @@ await getSession(); const adminUser = await getAdminUser();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminStops(): Promise<AdminStop[]> {
|
||||
async function getAdminStops(): Promise<AdminStop[]> {
|
||||
|
||||
await getSession(); const result = await getAdminOrders();
|
||||
if (!result.success) return [];
|
||||
return result.stops;
|
||||
}
|
||||
|
||||
export async function getAdminPendingOrders(): Promise<AdminOrder[]> {
|
||||
async function getAdminPendingOrders(): Promise<AdminOrder[]> {
|
||||
|
||||
await getSession(); const result = await getAdminOrders();
|
||||
if (!result.success) return [];
|
||||
return result.orders.filter((o) => !o.pickup_complete);
|
||||
}
|
||||
|
||||
export async function getAdminPickedUpOrders(): Promise<AdminOrder[]> {
|
||||
async function getAdminPickedUpOrders(): Promise<AdminOrder[]> {
|
||||
|
||||
await getSession(); const result = await getAdminOrders();
|
||||
if (!result.success) return [];
|
||||
@@ -181,7 +181,7 @@ await getSession(); const adminUser = await getAdminUser();
|
||||
* When the pickup flow is rebuilt against the SaaS schema, this should
|
||||
* be replaced by a Drizzle update on a `pickup_events` table.
|
||||
*/
|
||||
export async function toggleOrderPickupComplete(params: {
|
||||
async function toggleOrderPickupComplete(params: {
|
||||
orderId: string;
|
||||
pickupComplete: boolean;
|
||||
}): Promise<{ success: true } | { success: false; error: string }> {
|
||||
|
||||
@@ -87,7 +87,7 @@ await getSession(); const adminUser = await getAdminUser();
|
||||
* sync log query. Returns 0 when the caller isn't authorized — never
|
||||
* throws, so callers can render "0" while the auth check settles.
|
||||
*/
|
||||
export async function getSquareQueueCount(brandId: string): Promise<number> {
|
||||
async function getSquareQueueCount(brandId: string): Promise<number> {
|
||||
|
||||
await getSession(); const adminUser = await getAdminUser();
|
||||
if (!adminUser) return 0;
|
||||
|
||||
@@ -58,7 +58,7 @@ export type StorefrontData = {
|
||||
* Look up a brand by its public slug. Returns `null` if no brand matches
|
||||
* (which means the storefront slug is wrong, or the brand was renamed).
|
||||
*/
|
||||
export async function getStorefrontBrandBySlug(
|
||||
async function getStorefrontBrandBySlug(
|
||||
slug: string,
|
||||
): Promise<StorefrontBrand | null> {
|
||||
|
||||
@@ -76,7 +76,7 @@ await getSession(); const { rows } = await pool.query<StorefrontBrand>(
|
||||
* Public stops for a brand storefront. Filters out draft / private stops
|
||||
* and any stop whose date has already passed.
|
||||
*/
|
||||
export async function getStorefrontStops(brandId: string): Promise<StorefrontStop[]> {
|
||||
async function getStorefrontStops(brandId: string): Promise<StorefrontStop[]> {
|
||||
|
||||
await getSession(); const { rows } = await pool.query<StorefrontStop>(
|
||||
`SELECT id, brand_id, name, city, state, zip, address, location,
|
||||
@@ -101,7 +101,7 @@ await getSession(); const { rows } = await pool.query<StorefrontStop>(
|
||||
* float in **dollars** so callers can format directly as `$${price}`
|
||||
* without doing the cents-to-dollars math.
|
||||
*/
|
||||
export async function getStorefrontProducts(
|
||||
async function getStorefrontProducts(
|
||||
brandId: string,
|
||||
): Promise<StorefrontProduct[]> {
|
||||
|
||||
@@ -127,7 +127,7 @@ await getSession(); const { rows } = await pool.query<StorefrontProduct>(
|
||||
* Returns `{ brand: null, stops: [], products: [] }` if the slug doesn't
|
||||
* match any brand — pages fall back to default copy in that case.
|
||||
*/
|
||||
export async function getStorefrontData(slug: string): Promise<StorefrontData> {
|
||||
async function getStorefrontData(slug: string): Promise<StorefrontData> {
|
||||
|
||||
await getSession(); const brand = await getStorefrontBrandBySlug(slug);
|
||||
if (!brand) return { brand: null, stops: [], products: [] };
|
||||
@@ -151,7 +151,7 @@ export type StorefrontWholesaleSettings = {
|
||||
* fields don't exist on `wholesale_settings` yet and shouldn't be
|
||||
* surfaced here. If you need them, add a migration first.
|
||||
*/
|
||||
export async function getStorefrontWholesaleSettings(
|
||||
async function getStorefrontWholesaleSettings(
|
||||
slug: string,
|
||||
): Promise<StorefrontWholesaleSettings | null> {
|
||||
|
||||
@@ -178,7 +178,7 @@ export type WholesalePortalConfig = {
|
||||
* to show the online-payment button. Doesn't require auth (the portal
|
||||
* already validated the session before this is called).
|
||||
*/
|
||||
export async function getWholesalePortalConfig(
|
||||
async function getWholesalePortalConfig(
|
||||
brandId: string,
|
||||
): Promise<WholesalePortalConfig | null> {
|
||||
|
||||
@@ -199,7 +199,7 @@ export type BrandName = { name: string };
|
||||
* portal header to render the brand name without leaking any other
|
||||
* brand columns (logo, plan, stripe keys, etc.).
|
||||
*/
|
||||
export async function getStorefrontBrandName(
|
||||
async function getStorefrontBrandName(
|
||||
brandId: string,
|
||||
): Promise<string | null> {
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ export async function getWorkerTimeLogs(
|
||||
await getSession(); return [];
|
||||
}
|
||||
|
||||
export async function updateWorkerTimeLog(
|
||||
async function updateWorkerTimeLog(
|
||||
_logId: string,
|
||||
_taskName: string,
|
||||
_clockIn: string,
|
||||
@@ -181,7 +181,7 @@ await getSession(); const adminUser = await getAdminUser();
|
||||
return { success: false, error: "Time tracking is not configured" };
|
||||
}
|
||||
|
||||
export async function deleteWorkerTimeLog(_logId: string): Promise<{ success: boolean; error?: string }> {
|
||||
async function deleteWorkerTimeLog(_logId: string): Promise<{ success: boolean; error?: string }> {
|
||||
|
||||
await getSession(); const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
@@ -125,7 +125,7 @@ async function requireWaterAdminPermission() {
|
||||
* For the `/water/admin` (PIN) portal. Returns the admin + brand if a
|
||||
* valid session cookie is present. Falls back to a 401-shaped error.
|
||||
*/
|
||||
export async function requireWaterAdminSession(): Promise<
|
||||
async function requireWaterAdminSession(): Promise<
|
||||
| { ok: true; adminUserId: string; brandId: string }
|
||||
| { ok: false; error: string }
|
||||
> {
|
||||
|
||||
@@ -287,7 +287,7 @@ type FieldSession =
|
||||
| { ok: true; userId: string; brandId: string; role: "irrigator" | "water_admin" }
|
||||
| { ok: false; error: string };
|
||||
|
||||
export async function requireFieldSession(): Promise<FieldSession> {
|
||||
async function requireFieldSession(): Promise<FieldSession> {
|
||||
|
||||
await getSession(); const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("wl_session")?.value;
|
||||
@@ -322,7 +322,7 @@ await getSession(); const cookieStore = await cookies();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFieldSessionUser(): Promise<{
|
||||
async function getFieldSessionUser(): Promise<{
|
||||
userId: string;
|
||||
name: string;
|
||||
brandId: string;
|
||||
@@ -385,7 +385,7 @@ await getSession(); const cookieStore = await cookies();
|
||||
cookieStore.delete("wl_admin_session");
|
||||
}
|
||||
|
||||
export async function getWaterSession(): Promise<string | null> {
|
||||
async function getWaterSession(): Promise<string | null> {
|
||||
|
||||
await getSession(); const cookieStore = await cookies();
|
||||
return cookieStore.get("wl_session")?.value ?? null;
|
||||
|
||||
@@ -266,7 +266,7 @@ await getSession(); // Generate UUID at the start of checkout — before RPC ca
|
||||
};
|
||||
}
|
||||
|
||||
export async function enqueueWholesaleWebhookForOrderCreated(orderId: string, brandId: string, customerId: string, invoiceNumber: string | null, subtotal: number) {
|
||||
async function enqueueWholesaleWebhookForOrderCreated(orderId: string, brandId: string, customerId: string, invoiceNumber: string | null, subtotal: number) {
|
||||
|
||||
await getSession(); try {
|
||||
await pool.query(
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export function getNeonAuthConfig(): NeonAuthBaseConfig {
|
||||
* Returns the Neon Auth base config, or null if not configured.
|
||||
* Use this for middleware that should gracefully degrade if auth is not set up.
|
||||
*/
|
||||
export function getNeonAuthConfigOptional(): NeonAuthBaseConfig | null {
|
||||
function getNeonAuthConfigOptional(): NeonAuthBaseConfig | null {
|
||||
const baseUrl = process.env.NEON_AUTH_BASE_URL || "";
|
||||
const cookieSecret = process.env.NEON_AUTH_COOKIE_SECRET || "";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export type AIProvider = "openai" | "anthropic" | "google" | "xai" | "minimax" | "custom";
|
||||
|
||||
export const PROVIDER_MODELS: Record<Exclude<AIProvider, "custom">, string[]> = {
|
||||
const PROVIDER_MODELS: Record<Exclude<AIProvider, "custom">, string[]> = {
|
||||
openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"],
|
||||
// Note: claude-3-5-sonnet-* model IDs have been retired by Anthropic.
|
||||
// Current IDs are claude-sonnet-4-5, claude-sonnet-4-*, claude-opus-4-*, etc.
|
||||
@@ -37,6 +37,6 @@ export const DEFAULT_MODELS: Record<Exclude<AIProvider, "custom">, string> = {
|
||||
minimax: "MiniMax-M3",
|
||||
};
|
||||
|
||||
export function getModelsForProvider(provider: Exclude<AIProvider, "custom">): string[] {
|
||||
function getModelsForProvider(provider: Exclude<AIProvider, "custom">): string[] {
|
||||
return PROVIDER_MODELS[provider] ?? [];
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
|
||||
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com";
|
||||
|
||||
// Only enable in production or when API key is set
|
||||
export const posthogEnabled = Boolean(posthogApiKey);
|
||||
const posthogEnabled = Boolean(posthogApiKey);
|
||||
|
||||
// Analytics helper functions
|
||||
export const analytics = {
|
||||
@@ -118,13 +118,13 @@ export const analytics = {
|
||||
},
|
||||
};
|
||||
|
||||
export function identifyUser(_userId: string, _properties?: Record<string, unknown>) {
|
||||
function identifyUser(_userId: string, _properties?: Record<string, unknown>) {
|
||||
if (posthogEnabled && typeof window !== "undefined") {
|
||||
console.log("[Analytics] User identified");
|
||||
}
|
||||
}
|
||||
|
||||
export function groupByBrand(_brandId: string, _properties?: Record<string, unknown>) {
|
||||
function groupByBrand(_brandId: string, _properties?: Record<string, unknown>) {
|
||||
if (posthogEnabled && typeof window !== "undefined") {
|
||||
console.log("[Analytics] Grouped by brand");
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import "server-only";
|
||||
import { cookies } from "next/headers";
|
||||
import type { AdminUser } from "./admin-permissions-types";
|
||||
|
||||
export const ACTIVE_BRAND_COOKIE = "active_brand_id";
|
||||
const ACTIVE_BRAND_COOKIE = "active_brand_id";
|
||||
|
||||
/**
|
||||
* Resolve the active brand id for the given admin user.
|
||||
|
||||
@@ -152,7 +152,7 @@ const FIELD_COLUMNS: Record<string, Set<string>> = {
|
||||
external_id: EXTERNAL_ID_COLUMNS,
|
||||
};
|
||||
|
||||
export function detectColumnMappings(
|
||||
function detectColumnMappings(
|
||||
headers: string[],
|
||||
rows: string[][]
|
||||
): ColumnMapping[] {
|
||||
@@ -210,7 +210,7 @@ export function detectColumnMappings(
|
||||
return mappings;
|
||||
}
|
||||
|
||||
export function mapRowToContactEntry(
|
||||
function mapRowToContactEntry(
|
||||
row: string[],
|
||||
mappings: ColumnMapping[]
|
||||
): {
|
||||
|
||||
@@ -14,7 +14,7 @@ export function formatDate(iso: string | null | undefined): string {
|
||||
/**
|
||||
* Format an ISO datetime string as "Jan 5, 2025 at 2:30 PM"
|
||||
*/
|
||||
export function formatDateTime(iso: string | null | undefined): string {
|
||||
function formatDateTime(iso: string | null | undefined): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "—";
|
||||
@@ -26,7 +26,7 @@ export function formatDateTime(iso: string | null | undefined): string {
|
||||
/**
|
||||
* Format an ISO date string as YYYY-MM-DD (for form inputs, export filenames)
|
||||
*/
|
||||
export function formatDateISO(iso: string | null | undefined): string {
|
||||
function formatDateISO(iso: string | null | undefined): string {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
@@ -36,7 +36,7 @@ export function formatDateISO(iso: string | null | undefined): string {
|
||||
/**
|
||||
* Format as YYYY-MM-DDTHH:MM for datetime-local input min values
|
||||
*/
|
||||
export function formatDateTimeLocal(iso: string | null | undefined): string {
|
||||
function formatDateTimeLocal(iso: string | null | undefined): string {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "";
|
||||
@@ -46,7 +46,7 @@ export function formatDateTimeLocal(iso: string | null | undefined): string {
|
||||
/**
|
||||
* Relative time: "5m ago", "2h ago", "3d ago"
|
||||
*/
|
||||
export function timeAgo(iso: string | null | undefined): string {
|
||||
function timeAgo(iso: string | null | undefined): string {
|
||||
if (!iso) return "Never";
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
if (diff < 0) return "Just now";
|
||||
@@ -66,14 +66,14 @@ export function timeAgo(iso: string | null | undefined): string {
|
||||
/**
|
||||
* Month name from 0-based index (for selector options)
|
||||
*/
|
||||
export function getMonthName(monthIndex: number): string {
|
||||
function getMonthName(monthIndex: number): string {
|
||||
return new Date(0, monthIndex).toLocaleString("en-US", { month: "long" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Short month name: "Jan", "Feb"
|
||||
*/
|
||||
export function getMonthNameShort(monthIndex: number): string {
|
||||
function getMonthNameShort(monthIndex: number): string {
|
||||
return new Date(0, monthIndex).toLocaleString("en-US", { month: "short" });
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ export const ADDON_CATALOG: Record<BrandFeatureKey, BrandFeature> = {
|
||||
};
|
||||
|
||||
// Core features that cannot be disabled
|
||||
export const CORE_FEATURES: BrandFeatureKey[] = [
|
||||
const CORE_FEATURES: BrandFeatureKey[] = [
|
||||
// Core platform features (always enabled)
|
||||
// These are not add-ons — the platform doesn't function without them
|
||||
];
|
||||
@@ -197,6 +197,6 @@ async function fetchBrandFeatures(
|
||||
/**
|
||||
* Invalidate cached features for a brand (call after feature toggle).
|
||||
*/
|
||||
export function invalidateBrandFeatureCache(brandId: string): void {
|
||||
function invalidateBrandFeatureCache(brandId: string): void {
|
||||
brandFeatureCache.delete(brandId);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function formatDateTime(date: string | Date | null | undefined): string {
|
||||
});
|
||||
}
|
||||
|
||||
export function formatTime(date: string | Date | null | undefined): string {
|
||||
function formatTime(date: string | Date | null | undefined): string {
|
||||
if (!date) return "—";
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
if (isNaN(d.getTime())) return "—";
|
||||
|
||||
+10
-10
@@ -131,7 +131,7 @@ export type AddonKey = keyof typeof ADDONS;
|
||||
|
||||
// ── Feature list per tier (for comparison table) ────────────────────────────
|
||||
|
||||
export const TIER_FEATURE_MATRIX: Record<PlanTierKey, string[]> = {
|
||||
const TIER_FEATURE_MATRIX: Record<PlanTierKey, string[]> = {
|
||||
starter: [
|
||||
"Products catalog",
|
||||
"Stops management (10/mo)",
|
||||
@@ -168,35 +168,35 @@ export const TIER_FEATURE_MATRIX: Record<PlanTierKey, string[]> = {
|
||||
|
||||
// ── Utility functions ────────────────────────────────────────────────────────
|
||||
|
||||
export function formatPrice(amount: number | null): string {
|
||||
function formatPrice(amount: number | null): string {
|
||||
if (amount === null) return "Custom";
|
||||
return `$${amount.toFixed(0)}`;
|
||||
}
|
||||
|
||||
export function formatPricePerMonth(amount: number | null): string {
|
||||
function formatPricePerMonth(amount: number | null): string {
|
||||
if (amount === null) return "Custom";
|
||||
return `$${amount}/mo`;
|
||||
}
|
||||
|
||||
export function getPlanMonthlyPrice(tier: PlanTierKey): number | null {
|
||||
function getPlanMonthlyPrice(tier: PlanTierKey): number | null {
|
||||
return PLAN_TIERS[tier]?.monthlyPrice ?? null;
|
||||
}
|
||||
|
||||
export function getPlanAnnualPrice(tier: PlanTierKey): number | null {
|
||||
function getPlanAnnualPrice(tier: PlanTierKey): number | null {
|
||||
return PLAN_TIERS[tier]?.annualPrice ?? null;
|
||||
}
|
||||
|
||||
export function getAddonMonthlyPrice(addon: AddonKey): number {
|
||||
function getAddonMonthlyPrice(addon: AddonKey): number {
|
||||
return ADDONS[addon]?.monthlyPrice ?? 0;
|
||||
}
|
||||
|
||||
export function getAddonAnnualPrice(addon: AddonKey): number {
|
||||
function getAddonAnnualPrice(addon: AddonKey): number {
|
||||
return ADDONS[addon]?.annualPrice ?? 0;
|
||||
}
|
||||
|
||||
export function calculateAnnualSavings(monthlyTotal: number): number {
|
||||
function calculateAnnualSavings(monthlyTotal: number): number {
|
||||
return Math.round(monthlyTotal * 0.25); // 25% annual discount
|
||||
}
|
||||
|
||||
export const BILLING_COMPANY_NAME = "Cielo Hermosa, LLC";
|
||||
export const BILLING_EMAIL = "billing@cielohermosa.com";
|
||||
const BILLING_COMPANY_NAME = "Cielo Hermosa, LLC";
|
||||
const BILLING_EMAIL = "billing@cielohermosa.com";
|
||||
+3
-3
@@ -31,13 +31,13 @@ export function registerServiceWorker() {
|
||||
});
|
||||
}
|
||||
|
||||
export async function subscribeToPush(_registration: ServiceWorkerRegistration) {
|
||||
async function subscribeToPush(_registration: ServiceWorkerRegistration) {
|
||||
// Push subscription disabled for now - requires VAPID key setup
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if app is installed (PWA)
|
||||
export function isPWAInstalled(): boolean {
|
||||
function isPWAInstalled(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
@@ -46,7 +46,7 @@ export function isPWAInstalled(): boolean {
|
||||
}
|
||||
|
||||
// Share API for native sharing
|
||||
export async function shareContent(content: {
|
||||
async function shareContent(content: {
|
||||
title: string;
|
||||
text: string;
|
||||
url?: string;
|
||||
|
||||
@@ -76,7 +76,7 @@ export function securityHeaders() {
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function rateLimitHeaders(rateCheck: { remaining: number; reset: number }) {
|
||||
function rateLimitHeaders(rateCheck: { remaining: number; reset: number }) {
|
||||
return {
|
||||
"X-RateLimit-Limit": String(100),
|
||||
"X-RateLimit-Remaining": String(rateCheck.remaining),
|
||||
@@ -86,6 +86,6 @@ export function rateLimitHeaders(rateCheck: { remaining: number; reset: number }
|
||||
|
||||
// Pre-configured limiters
|
||||
export const apiLimiter = RATE_LIMITS.api;
|
||||
export const checkoutLimiter = RATE_LIMITS.checkout;
|
||||
const checkoutLimiter = RATE_LIMITS.checkout;
|
||||
export const emailLimiter = RATE_LIMITS.email;
|
||||
export const authLimiter = RATE_LIMITS.auth;
|
||||
const authLimiter = RATE_LIMITS.auth;
|
||||
+3
-3
@@ -53,7 +53,7 @@ export const captureError = (error: Error, context?: Record<string, unknown>) =>
|
||||
};
|
||||
|
||||
// Export for manual breadcrumb logging
|
||||
export const addBreadcrumb = (message: string, data?: Record<string, unknown>) => {
|
||||
const addBreadcrumb = (message: string, data?: Record<string, unknown>) => {
|
||||
Sentry.addBreadcrumb({
|
||||
message,
|
||||
data,
|
||||
@@ -62,7 +62,7 @@ export const addBreadcrumb = (message: string, data?: Record<string, unknown>) =
|
||||
};
|
||||
|
||||
// Export for user tracking during errors
|
||||
export const setUserContext = (userId: string, brandId?: string) => {
|
||||
const setUserContext = (userId: string, brandId?: string) => {
|
||||
Sentry.setUser({
|
||||
id: userId,
|
||||
tags: { brand_id: brandId || "unknown" },
|
||||
@@ -70,7 +70,7 @@ export const setUserContext = (userId: string, brandId?: string) => {
|
||||
};
|
||||
|
||||
// Export for transaction tracing - simplified wrapper
|
||||
export async function withTransaction<T>(
|
||||
async function withTransaction<T>(
|
||||
name: string,
|
||||
op: string,
|
||||
fn: () => Promise<T>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function serverLog(...args: unknown[]): void {
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
export function serverInfo(...args: unknown[]): void {
|
||||
function serverInfo(...args: unknown[]): void {
|
||||
console.info(...args);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -22,7 +22,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
let _client: S3Client | null = null;
|
||||
|
||||
export function getStorageClient(): S3Client {
|
||||
function getStorageClient(): S3Client {
|
||||
if (_client) return _client;
|
||||
|
||||
const endpoint = process.env.MINIO_ENDPOINT ?? "s3.crispygoat.com";
|
||||
@@ -48,7 +48,7 @@ export function getStorageClient(): S3Client {
|
||||
* Returns the public base URL for the given bucket.
|
||||
* Override with MINIO_PUBLIC_URL if your public endpoint differs from the endpoint.
|
||||
*/
|
||||
export function getPublicUrl(bucket: string, key: string): string {
|
||||
function getPublicUrl(bucket: string, key: string): string {
|
||||
const base = process.env.MINIO_PUBLIC_URL ?? `https://${process.env.MINIO_ENDPOINT ?? "s3.crispygoat.com"}`;
|
||||
return `${base}/${bucket}/${key}`;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export async function uploadObject(opts: UploadOptions): Promise<string> {
|
||||
return getPublicUrl(opts.bucket, opts.key);
|
||||
}
|
||||
|
||||
export async function deleteObject(bucket: string, key: string): Promise<void> {
|
||||
async function deleteObject(bucket: string, key: string): Promise<void> {
|
||||
const client = getStorageClient();
|
||||
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export async function deleteObject(bucket: string, key: string): Promise<void> {
|
||||
* @param contentType Expected MIME type
|
||||
* @param expiresSeconds URL expiry (default 5 minutes)
|
||||
*/
|
||||
export async function getPresignedPutUrl(
|
||||
async function getPresignedPutUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
contentType: string,
|
||||
@@ -118,7 +118,7 @@ export async function getPresignedPutUrl(
|
||||
* @param key Object key
|
||||
* @param expiresSeconds URL expiry (default 1 hour)
|
||||
*/
|
||||
export async function getPresignedGetUrl(
|
||||
async function getPresignedGetUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
expiresSeconds = 3600
|
||||
|
||||
+23
-23
@@ -33,7 +33,7 @@ export const stripe = {
|
||||
};
|
||||
|
||||
// Plan tier configurations
|
||||
export const PLANS = {
|
||||
const PLANS = {
|
||||
starter: {
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
@@ -99,7 +99,7 @@ export const PLANS = {
|
||||
} as const;
|
||||
|
||||
// Add-ons configuration
|
||||
export const ADDONS = {
|
||||
const ADDONS = {
|
||||
wholesale_portal: {
|
||||
id: "wholesale_portal",
|
||||
name: "Wholesale Portal",
|
||||
@@ -168,7 +168,7 @@ export interface CreateSubscriptionOptions {
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function createSubscription(options: CreateSubscriptionOptions) {
|
||||
async function createSubscription(options: CreateSubscriptionOptions) {
|
||||
const { brandId, brandName, email, plan, interval, successUrl, cancelUrl, metadata } = options;
|
||||
|
||||
// Get or create Stripe customer
|
||||
@@ -212,7 +212,7 @@ export async function createSubscription(options: CreateSubscriptionOptions) {
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function createAddonSubscription(options: {
|
||||
async function createAddonSubscription(options: {
|
||||
customerId: string;
|
||||
addon: keyof typeof ADDONS;
|
||||
brandId: string;
|
||||
@@ -256,7 +256,7 @@ export async function createAddonSubscription(options: {
|
||||
// CUSTOMER PORTAL
|
||||
// ============================================
|
||||
|
||||
export async function createCustomerPortalSession(options: {
|
||||
async function createCustomerPortalSession(options: {
|
||||
customerId: string;
|
||||
returnUrl: string;
|
||||
}) {
|
||||
@@ -274,7 +274,7 @@ export async function createCustomerPortalSession(options: {
|
||||
// SUBSCRIPTION UPDATES
|
||||
// ============================================
|
||||
|
||||
export async function cancelSubscription(subscriptionId: string, immediate: boolean = false) {
|
||||
async function cancelSubscription(subscriptionId: string, immediate: boolean = false) {
|
||||
if (immediate) {
|
||||
return await stripe.subscriptions.cancel(subscriptionId);
|
||||
}
|
||||
@@ -284,13 +284,13 @@ export async function cancelSubscription(subscriptionId: string, immediate: bool
|
||||
});
|
||||
}
|
||||
|
||||
export async function reactivateSubscription(subscriptionId: string) {
|
||||
async function reactivateSubscription(subscriptionId: string) {
|
||||
return await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function changePlan(subscriptionId: string, newPlan: keyof typeof PLANS, interval: "monthly" | "annual") {
|
||||
async function changePlan(subscriptionId: string, newPlan: keyof typeof PLANS, interval: "monthly" | "annual") {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
// Get new price ID
|
||||
@@ -314,7 +314,7 @@ export async function changePlan(subscriptionId: string, newPlan: keyof typeof P
|
||||
// CUSTOMER MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
export async function getOrCreateCustomer(brandId: string, name: string, email: string) {
|
||||
async function getOrCreateCustomer(brandId: string, name: string, email: string) {
|
||||
// Check if customer exists for this brand
|
||||
const existingCustomers = await stripe.customers.list({
|
||||
email,
|
||||
@@ -337,11 +337,11 @@ export async function getOrCreateCustomer(brandId: string, name: string, email:
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
export async function getCustomer(customerId: string) {
|
||||
async function getCustomer(customerId: string) {
|
||||
return await stripe.customers.retrieve(customerId);
|
||||
}
|
||||
|
||||
export async function updateCustomer(customerId: string, data: Partial<Stripe.CustomerUpdateParams>) {
|
||||
async function updateCustomer(customerId: string, data: Partial<Stripe.CustomerUpdateParams>) {
|
||||
return await stripe.customers.update(customerId, data);
|
||||
}
|
||||
|
||||
@@ -349,20 +349,20 @@ export async function updateCustomer(customerId: string, data: Partial<Stripe.Cu
|
||||
// PAYMENT METHODS
|
||||
// ============================================
|
||||
|
||||
export async function listPaymentMethods(customerId: string) {
|
||||
async function listPaymentMethods(customerId: string) {
|
||||
return await stripe.paymentMethods.list({
|
||||
customer: customerId,
|
||||
type: "card",
|
||||
});
|
||||
}
|
||||
|
||||
export async function attachPaymentMethod(paymentMethodId: string, customerId: string) {
|
||||
async function attachPaymentMethod(paymentMethodId: string, customerId: string) {
|
||||
return await stripe.paymentMethods.attach(paymentMethodId, {
|
||||
customer: customerId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function setDefaultPaymentMethod(customerId: string, paymentMethodId: string) {
|
||||
async function setDefaultPaymentMethod(customerId: string, paymentMethodId: string) {
|
||||
return await stripe.customers.update(customerId, {
|
||||
invoice_settings: {
|
||||
default_payment_method: paymentMethodId,
|
||||
@@ -374,18 +374,18 @@ export async function setDefaultPaymentMethod(customerId: string, paymentMethodI
|
||||
// INVOICES
|
||||
// ============================================
|
||||
|
||||
export async function listInvoices(customerId: string, limit: number = 24) {
|
||||
async function listInvoices(customerId: string, limit: number = 24) {
|
||||
return await stripe.invoices.list({
|
||||
customer: customerId,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getInvoice(invoiceId: string) {
|
||||
async function getInvoice(invoiceId: string) {
|
||||
return await stripe.invoices.retrieve(invoiceId);
|
||||
}
|
||||
|
||||
export async function getUpcomingInvoice(customerId: string) {
|
||||
async function getUpcomingInvoice(customerId: string) {
|
||||
// Use Stripe's upcoming invoice preview endpoint
|
||||
return await stripe.invoices.createPreview({
|
||||
customer: customerId,
|
||||
@@ -396,7 +396,7 @@ export async function getUpcomingInvoice(customerId: string) {
|
||||
// PAYMENTS & REFUNDS
|
||||
// ============================================
|
||||
|
||||
export async function createPaymentIntent(options: {
|
||||
async function createPaymentIntent(options: {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
customerId?: string;
|
||||
@@ -413,7 +413,7 @@ export async function createPaymentIntent(options: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRefund(paymentIntentId: string, amount?: number, reason?: string) {
|
||||
async function createRefund(paymentIntentId: string, amount?: number, reason?: string) {
|
||||
return await stripe.refunds.create({
|
||||
payment_intent: paymentIntentId,
|
||||
amount,
|
||||
@@ -613,7 +613,7 @@ async function disableAddonFeature(brandId: string | undefined, addonKey: string
|
||||
// USAGE BILLING
|
||||
// ============================================
|
||||
|
||||
export async function createUsageRecord(options: {
|
||||
async function createUsageRecord(options: {
|
||||
subscriptionItemId: string;
|
||||
quantity: number;
|
||||
timestamp?: number;
|
||||
@@ -640,7 +640,7 @@ export async function createUsageRecord(options: {
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUsageSummary(subscriptionItemId: string) {
|
||||
async function getUsageSummary(subscriptionItemId: string) {
|
||||
// List subscription items with usage summaries
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
subscriptionItemId.split("_")[0]
|
||||
@@ -653,11 +653,11 @@ export async function getUsageSummary(subscriptionItemId: string) {
|
||||
// EXPORT STRIPE INSTANCE AND HELPERS
|
||||
// ============================================
|
||||
|
||||
export async function getSubscription(subscriptionId: string) {
|
||||
async function getSubscription(subscriptionId: string) {
|
||||
return await stripe.subscriptions.retrieve(subscriptionId);
|
||||
}
|
||||
|
||||
export async function listSubscriptions(customerId: string) {
|
||||
async function listSubscriptions(customerId: string) {
|
||||
return await stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
status: "all",
|
||||
|
||||
@@ -35,7 +35,7 @@ export type DailyReportOptions = {
|
||||
previewMode?: boolean;
|
||||
};
|
||||
|
||||
export const WATER_LOG_DISPLAY_REFRESH_MS = 30_000;
|
||||
const WATER_LOG_DISPLAY_REFRESH_MS = 30_000;
|
||||
|
||||
export function getDisplayAgeLabel(minutes: number | null): string {
|
||||
if (minutes === null) return "never";
|
||||
|
||||
Reference in New Issue
Block a user