feat: KRW 가격 지원 및 GPU/G8/VPU 인스턴스 추가
## KRW 가격 기능 - pricing 테이블에 hourly_price_krw, monthly_price_krw 컬럼 추가 - 부가세 10% + 영업이익 10% + 환율 적용 (기본 1450원) - 시간당: 1원 단위 반올림 (최소 1원) - 월간: 100원 단위 반올림 (최소 100원) - 환율/부가세/영업이익률 환경변수로 분리 (배포 없이 변경 가능) ## GPU/G8/VPU 인스턴스 지원 - gpu_instances, gpu_pricing 테이블 추가 - g8_instances, g8_pricing 테이블 추가 - vpu_instances, vpu_pricing 테이블 추가 - Linode/Vultr 커넥터에 GPU 동기화 로직 추가 ## 환경변수 추가 - KRW_EXCHANGE_RATE: 환율 (기본 1450) - KRW_VAT_RATE: 부가세율 (기본 1.1) - KRW_MARKUP_RATE: 영업이익률 (기본 1.1) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
138
src/repositories/g8-pricing.ts
Normal file
138
src/repositories/g8-pricing.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* G8 Pricing Repository
|
||||
* Handles CRUD operations for G8 instance pricing data
|
||||
*/
|
||||
|
||||
import { BaseRepository } from './base';
|
||||
import { G8Pricing, G8PricingInput, RepositoryError, ErrorCodes, Env } from '../types';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { calculateKRWHourly, calculateKRWMonthly } from '../constants';
|
||||
|
||||
export class G8PricingRepository extends BaseRepository<G8Pricing> {
|
||||
protected tableName = 'g8_pricing';
|
||||
protected logger = createLogger('[G8PricingRepository]');
|
||||
protected allowedColumns = [
|
||||
'g8_instance_id',
|
||||
'region_id',
|
||||
'hourly_price',
|
||||
'monthly_price',
|
||||
'hourly_price_krw',
|
||||
'monthly_price_krw',
|
||||
'currency',
|
||||
'available',
|
||||
];
|
||||
|
||||
constructor(db: D1Database, private env?: Env) {
|
||||
super(db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all pricing records for a specific G8 instance
|
||||
*/
|
||||
async findByG8Instance(g8InstanceId: number): Promise<G8Pricing[]> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.prepare('SELECT * FROM g8_pricing WHERE g8_instance_id = ?')
|
||||
.bind(g8InstanceId)
|
||||
.all<G8Pricing>();
|
||||
|
||||
return result.results;
|
||||
} catch (error) {
|
||||
this.logger.error('findByG8Instance failed', {
|
||||
g8InstanceId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw new RepositoryError(
|
||||
`Failed to find pricing for G8 instance: ${g8InstanceId}`,
|
||||
ErrorCodes.DATABASE_ERROR,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find pricing for a specific G8 instance in a specific region
|
||||
*/
|
||||
async findByInstanceAndRegion(g8InstanceId: number, regionId: number): Promise<G8Pricing | null> {
|
||||
try {
|
||||
const result = await this.db
|
||||
.prepare('SELECT * FROM g8_pricing WHERE g8_instance_id = ? AND region_id = ?')
|
||||
.bind(g8InstanceId, regionId)
|
||||
.first<G8Pricing>();
|
||||
|
||||
return result || null;
|
||||
} catch (error) {
|
||||
this.logger.error('findByInstanceAndRegion failed', {
|
||||
g8InstanceId,
|
||||
regionId,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw new RepositoryError(
|
||||
`Failed to find pricing for G8 instance ${g8InstanceId} in region ${regionId}`,
|
||||
ErrorCodes.DATABASE_ERROR,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert multiple G8 pricing records (batch operation)
|
||||
*/
|
||||
async upsertMany(pricingData: G8PricingInput[]): Promise<number> {
|
||||
if (pricingData.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const statements = pricingData.map((pricing) => {
|
||||
const hourlyKrw = calculateKRWHourly(pricing.hourly_price, this.env);
|
||||
const monthlyKrw = calculateKRWMonthly(pricing.monthly_price, this.env);
|
||||
|
||||
return this.db.prepare(
|
||||
`INSERT INTO g8_pricing (
|
||||
g8_instance_id, region_id, hourly_price, monthly_price,
|
||||
hourly_price_krw, monthly_price_krw, currency, available
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(g8_instance_id, region_id)
|
||||
DO UPDATE SET
|
||||
hourly_price = excluded.hourly_price,
|
||||
monthly_price = excluded.monthly_price,
|
||||
hourly_price_krw = excluded.hourly_price_krw,
|
||||
monthly_price_krw = excluded.monthly_price_krw,
|
||||
currency = excluded.currency,
|
||||
available = excluded.available,
|
||||
updated_at = datetime('now')`
|
||||
).bind(
|
||||
pricing.g8_instance_id,
|
||||
pricing.region_id,
|
||||
pricing.hourly_price,
|
||||
pricing.monthly_price,
|
||||
hourlyKrw,
|
||||
monthlyKrw,
|
||||
pricing.currency,
|
||||
pricing.available
|
||||
);
|
||||
});
|
||||
|
||||
const results = await this.db.batch(statements);
|
||||
const successCount = results.filter((r) => r.success).length;
|
||||
|
||||
this.logger.info('upsertMany completed', {
|
||||
total: pricingData.length,
|
||||
success: successCount,
|
||||
});
|
||||
|
||||
return successCount;
|
||||
} catch (error) {
|
||||
this.logger.error('upsertMany failed', {
|
||||
count: pricingData.length,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
throw new RepositoryError(
|
||||
'Failed to upsert G8 pricing records',
|
||||
ErrorCodes.TRANSACTION_FAILED,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user