refactor: simplify pricing tables to USD-only

- Remove KRW pricing calculations from all pricing tables
- Simplify pricing table to store only wholesale USD prices
- Simplify anvil_pricing to store only retail USD prices
- Remove KRW environment variables (KRW_EXCHANGE_RATE, KRW_MARGIN_RATE)
- Remove KRW functions from constants.ts
- Update GPU/G8/VPU pricing repositories to match
- Add Anvil tables and repositories for branded product support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-25 21:16:25 +09:00
parent a2133ae5c9
commit 9f3d3a245a
21 changed files with 1952 additions and 197 deletions

View File

@@ -64,8 +64,6 @@ interface RawQueryResult {
pricing_region_id: number | null;
hourly_price: number | null;
monthly_price: number | null;
hourly_price_krw: number | null;
monthly_price_krw: number | null;
currency: string | null;
pricing_available: number | null;
pricing_created_at: string | null;
@@ -192,8 +190,6 @@ export class QueryService {
pr.region_id as pricing_region_id,
pr.hourly_price,
pr.monthly_price,
pr.hourly_price_krw,
pr.monthly_price_krw,
pr.currency,
pr.available as pricing_available,
pr.created_at as pricing_created_at,
@@ -380,8 +376,6 @@ export class QueryService {
region_id: row.pricing_region_id,
hourly_price: row.hourly_price,
monthly_price: row.monthly_price,
hourly_price_krw: row.hourly_price_krw,
monthly_price_krw: row.monthly_price_krw,
currency: row.currency,
available: row.pricing_available,
created_at: row.pricing_created_at,

View File

@@ -18,6 +18,7 @@ import { VultrConnector } from '../connectors/vultr';
import { AWSConnector } from '../connectors/aws';
import { RepositoryFactory } from '../repositories';
import { createLogger } from '../utils/logger';
import { calculateRetailHourly, calculateRetailMonthly } from '../constants';
import type {
Env,
ProviderSyncResult,
@@ -292,7 +293,23 @@ export class SyncOrchestrator {
}
this.logger.info(`${provider}${stage}`);
// Stage 8: Complete - Update provider status to success
// Stage 8: Sync Anvil Pricing (if applicable)
stage = 'SYNC_ANVIL_PRICING' as SyncStage;
let anvilPricingCount = 0;
try {
anvilPricingCount = await this.syncAnvilPricing(provider);
if (anvilPricingCount > 0) {
this.logger.info(`${provider}${stage}`, { anvil_pricing: anvilPricingCount });
}
} catch (anvilError) {
// Log error but don't fail the entire sync
this.logger.error('Anvil pricing sync failed', {
provider,
error: anvilError instanceof Error ? anvilError.message : String(anvilError)
});
}
// Stage 9: Complete - Update provider status to success
stage = SyncStage.COMPLETE;
await this.repos.providers.updateSyncStatus(provider, 'success');
@@ -832,6 +849,167 @@ export class SyncOrchestrator {
}
}
/**
* Synchronize Anvil pricing based on source provider pricing
*
* Updates anvil_pricing table with retail prices calculated from source pricing
* Formula: retail = cost × 1.21 (10% margin + 10% VAT)
*
* @param provider - Source provider name (linode, vultr, aws)
* @returns Number of anvil_pricing records updated
*/
private async syncAnvilPricing(provider: string): Promise<number> {
this.logger.info('Starting Anvil pricing sync', { provider });
try {
// Step 1: Find all anvil_regions sourced from this provider
const anvilRegionsResult = await this.repos.db
.prepare('SELECT id, source_region_id FROM anvil_regions WHERE source_provider = ?')
.bind(provider)
.all<{ id: number; source_region_id: number }>();
if (!anvilRegionsResult.success || anvilRegionsResult.results.length === 0) {
this.logger.info('No anvil_regions found for provider', { provider });
return 0;
}
const anvilRegions = anvilRegionsResult.results;
this.logger.info('Found anvil_regions', { provider, count: anvilRegions.length });
// Step 2: Find all anvil_pricing records with source_instance_id
const anvilPricingResult = await this.repos.db
.prepare(`
SELECT
ap.id,
ap.anvil_instance_id,
ap.anvil_region_id,
ap.source_instance_id,
ar.source_region_id
FROM anvil_pricing ap
JOIN anvil_regions ar ON ap.anvil_region_id = ar.id
WHERE ar.source_provider = ?
AND ap.source_instance_id IS NOT NULL
`)
.bind(provider)
.all<{
id: number;
anvil_instance_id: number;
anvil_region_id: number;
source_instance_id: number;
source_region_id: number;
}>();
if (!anvilPricingResult.success || anvilPricingResult.results.length === 0) {
this.logger.info('No anvil_pricing records found with source_instance_id', { provider });
return 0;
}
const anvilPricingRecords = anvilPricingResult.results;
this.logger.info('Found anvil_pricing records to update', {
provider,
count: anvilPricingRecords.length
});
// Step 4: Fetch source pricing data in batch
const sourcePricingResult = await this.repos.db
.prepare(`
SELECT
instance_type_id,
region_id,
hourly_price,
monthly_price
FROM pricing
WHERE instance_type_id IN (${anvilPricingRecords.map(() => '?').join(',')})
AND region_id IN (${anvilPricingRecords.map(() => '?').join(',')})
`)
.bind(
...anvilPricingRecords.map(r => r.source_instance_id),
...anvilPricingRecords.map(r => r.source_region_id)
)
.all<{
instance_type_id: number;
region_id: number;
hourly_price: number;
monthly_price: number;
}>();
if (!sourcePricingResult.success || sourcePricingResult.results.length === 0) {
this.logger.warn('No source pricing data found', { provider });
return 0;
}
// Step 5: Build lookup map: `${instance_type_id}_${region_id}` → pricing
const sourcePricingMap = new Map<string, { hourly_price: number; monthly_price: number }>(
sourcePricingResult.results.map(p => [
`${p.instance_type_id}_${p.region_id}`,
{ hourly_price: p.hourly_price, monthly_price: p.monthly_price }
])
);
// Step 6: Prepare update statements
const updateStatements: D1PreparedStatement[] = [];
for (const record of anvilPricingRecords) {
const lookupKey = `${record.source_instance_id}_${record.source_region_id}`;
const sourcePricing = sourcePricingMap.get(lookupKey);
if (!sourcePricing) {
this.logger.warn('Source pricing not found', {
anvil_pricing_id: record.id,
source_instance_id: record.source_instance_id,
source_region_id: record.source_region_id
});
continue;
}
// Calculate retail prices: cost × 1.21
const hourlyPrice = calculateRetailHourly(sourcePricing.hourly_price);
const monthlyPrice = calculateRetailMonthly(sourcePricing.monthly_price);
updateStatements.push(
this.repos.db.prepare(`
UPDATE anvil_pricing
SET
hourly_price = ?,
monthly_price = ?
WHERE id = ?
`).bind(
hourlyPrice,
monthlyPrice,
record.id
)
);
}
if (updateStatements.length === 0) {
this.logger.info('No anvil_pricing records to update', { provider });
return 0;
}
// Step 7: Execute batch update
const results = await this.repos.db.batch(updateStatements);
const successCount = results.reduce(
(sum, result) => sum + (result.meta?.changes ?? 0),
0
);
this.logger.info('Anvil pricing sync completed', {
provider,
updated: successCount,
total: updateStatements.length
});
return successCount;
} catch (error) {
this.logger.error('Anvil pricing sync failed', {
provider,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
/**
* Create connector for a specific provider
*