Feature: Add database-driven shipping rates management

- Add ShippingRate model to Prisma schema with service name, base price, per-kg surcharge, estimated delivery days, and international flag
- Create migration to add shipping_rates table
- Update seed.ts to populate initial Royal Mail rates (domestic and international)
- Refactor getRoyalMailShippingQuotes to fetch rates from database with fallback to defaults
- Add /admin/shipping-rates page for managing rates
- Add ShippingRatesForm component with inline editing and add/remove functionality
- Add server actions for saving and deleting rates
- Add "Shipping rates" link to admin navigation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-19 11:30:31 +01:00
co-authored by Claude Haiku 4.5
parent 617a5acbb2
commit f770428556
8 changed files with 485 additions and 72 deletions
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "ShippingRate" (
"id" TEXT NOT NULL,
"service" TEXT NOT NULL,
"basePrice" INTEGER NOT NULL,
"pricePerKg" INTEGER NOT NULL DEFAULT 0,
"estimatedDays" INTEGER NOT NULL,
"isInternational" BOOLEAN NOT NULL DEFAULT false,
"active" BOOLEAN NOT NULL DEFAULT true,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ShippingRate_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ShippingRate_service_key" ON "ShippingRate"("service");
+14
View File
@@ -546,3 +546,17 @@ model BankReconciliation {
confidence Int @default(100) // 0-100, how confident the match is (100=exact amount+date, <100=fuzzy match)
notes String? // manual notes about the match
}
// Shipping rates configuration — managed from admin panel
// Allows easy updates to Royal Mail pricing without code changes
model ShippingRate {
id String @id @default(cuid())
service String @unique // e.g., "Royal Mail 24®", "International Signed"
basePrice Int // cents — base price for this service
pricePerKg Int @default(0) // cents — additional cost per kg (mainly for international)
estimatedDays Int // estimated delivery days
isInternational Boolean @default(false)
active Boolean @default(true)
updatedAt DateTime @updatedAt
}
+49 -1
View File
@@ -123,6 +123,44 @@ const products: Array<{
},
];
const shippingRates = [
{
service: 'Special Delivery Guaranteed by 9am',
basePrice: 895,
pricePerKg: 0,
estimatedDays: 1,
isInternational: false,
},
{
service: 'Royal Mail 24®',
basePrice: 495,
pricePerKg: 0,
estimatedDays: 1,
isInternational: false,
},
{
service: 'Royal Mail 48®',
basePrice: 295,
pricePerKg: 0,
estimatedDays: 3,
isInternational: false,
},
{
service: 'International Signed',
basePrice: 1495,
pricePerKg: 300,
estimatedDays: 14,
isInternational: true,
},
{
service: 'International Economy',
basePrice: 1095,
pricePerKg: 200,
estimatedDays: 21,
isInternational: true,
},
];
async function main() {
for (const c of categories) {
await prisma.category.upsert({
@@ -153,7 +191,17 @@ async function main() {
});
}
console.log(`Seeded ${categories.length} categories and ${products.length} products.`);
for (const rate of shippingRates) {
await prisma.shippingRate.upsert({
where: { service: rate.service },
update: { ...rate },
create: { ...rate },
});
}
console.log(
`Seeded ${categories.length} categories, ${products.length} products, and ${shippingRates.length} shipping rates.`
);
}
main()