Files
goodgo-platform/prisma/scripts/seed-plans.ts
Ho Ngoc Hai 7e2ccdfb7c feat(web): add mobile swipe gestures to image gallery
Install react-swipeable and wire useSwipeable onto the main image
container — left-swipe advances to next image, right-swipe goes back.
Gestures only activate when there are multiple images; desktop button
navigation is fully preserved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-22 23:31:31 +07:00

216 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* seed-plans.ts
*
* Seeds all 4 subscription plan tiers with Vietnamese market pricing.
* Called from prisma/seed.ts Phase 1 before any other data is inserted.
* Idempotent — uses upsert on the unique `tier` field.
*
* Pricing (monthly / yearly):
* FREE — 0 / 0 VND
* AGENT_PRO — 499,000 / 4,990,000 VND (~$20/month, market-competitive)
* INVESTOR — 999,000 / 9,990,000 VND
* ENTERPRISE — 4,990,000 / 49,900,000 VND
*/
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient, PlanTier } from '@prisma/client';
import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env['DATABASE_URL'] });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const PLANS: Array<{
id: string;
tier: PlanTier;
name: string;
priceMonthlyVND: bigint;
priceYearlyVND: bigint;
maxListings: number | null;
maxSavedSearches: number | null;
maxAnalyticsQueries: number | null;
maxReports: number | null;
maxMediaUploads: number | null;
featuredListingsQuota: number | null;
features: Record<string, unknown>;
isActive: boolean;
}> = [
{
id: 'plan-free',
tier: PlanTier.FREE,
name: 'Miễn phí',
priceMonthlyVND: 0n,
priceYearlyVND: 0n,
maxListings: 3,
maxSavedSearches: 5,
maxAnalyticsQueries: 0,
maxReports: 0,
maxMediaUploads: 15, // 3 listings × 5 photos
featuredListingsQuota: 0,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 5,
analytics: false,
prioritySupport: false,
aiValuation: false,
featuredListing: false,
leadManagement: false,
agentProfile: false,
marketReports: false,
priceAlerts: false,
portfolioTracking: false,
apiAccess: false,
whiteLabel: false,
dedicatedSupport: false,
},
isActive: true,
},
{
id: 'plan-agent-pro',
tier: PlanTier.AGENT_PRO,
name: 'Agent Pro',
priceMonthlyVND: 499_000n,
priceYearlyVND: 4_990_000n,
maxListings: 50,
maxSavedSearches: 30,
maxAnalyticsQueries: 500,
maxReports: 20,
maxMediaUploads: 1_500, // 50 listings × 30 photos
featuredListingsQuota: 5,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 30,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
marketReports: false,
priceAlerts: false,
portfolioTracking: false,
apiAccess: false,
whiteLabel: false,
dedicatedSupport: false,
},
isActive: true,
},
{
id: 'plan-investor',
tier: PlanTier.INVESTOR,
name: 'Investor',
priceMonthlyVND: 999_000n,
priceYearlyVND: 9_990_000n,
maxListings: 20,
maxSavedSearches: 100,
maxAnalyticsQueries: 2_000,
maxReports: 100,
maxMediaUploads: 300, // 20 listings × 15 photos
featuredListingsQuota: 0,
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 15,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: false,
leadManagement: false,
agentProfile: false,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
apiAccess: false,
whiteLabel: false,
dedicatedSupport: false,
},
isActive: true,
},
{
id: 'plan-enterprise',
tier: PlanTier.ENTERPRISE,
name: 'Enterprise',
priceMonthlyVND: 4_990_000n,
priceYearlyVND: 49_900_000n,
maxListings: null, // unlimited
maxSavedSearches: null, // unlimited
maxAnalyticsQueries: null, // unlimited
maxReports: null, // unlimited
maxMediaUploads: null, // unlimited
featuredListingsQuota: null, // unlimited
features: {
basicSearch: true,
listingPost: true,
maxPhotos: 100,
analytics: true,
prioritySupport: true,
aiValuation: true,
featuredListing: true,
leadManagement: true,
agentProfile: true,
marketReports: true,
priceAlerts: true,
portfolioTracking: true,
apiAccess: true,
whiteLabel: true,
dedicatedSupport: true,
},
isActive: true,
},
];
export async function seedPlans(): Promise<void> {
console.log('💳 Seeding subscription plans...');
for (const plan of PLANS) {
await prisma.plan.upsert({
where: { tier: plan.tier },
update: {
name: plan.name,
priceMonthlyVND: plan.priceMonthlyVND,
priceYearlyVND: plan.priceYearlyVND,
maxListings: plan.maxListings,
maxSavedSearches: plan.maxSavedSearches,
maxAnalyticsQueries: plan.maxAnalyticsQueries,
maxReports: plan.maxReports,
maxMediaUploads: plan.maxMediaUploads,
featuredListingsQuota: plan.featuredListingsQuota,
features: plan.features,
isActive: plan.isActive,
},
create: {
id: plan.id,
tier: plan.tier,
name: plan.name,
priceMonthlyVND: plan.priceMonthlyVND,
priceYearlyVND: plan.priceYearlyVND,
maxListings: plan.maxListings,
maxSavedSearches: plan.maxSavedSearches,
maxAnalyticsQueries: plan.maxAnalyticsQueries,
maxReports: plan.maxReports,
maxMediaUploads: plan.maxMediaUploads,
featuredListingsQuota: plan.featuredListingsQuota,
features: plan.features,
isActive: plan.isActive,
},
});
}
console.log(`${PLANS.length} plans seeded (FREE, AGENT_PRO, INVESTOR, ENTERPRISE)`);
}
// Allow running standalone: `npx ts-node prisma/scripts/seed-plans.ts`
if (require.main === module) {
seedPlans()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
await pool.end();
});
}