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:
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user