Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 92 additions & 14 deletions app/src/adapters/PolicyAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,110 @@
import { V2PolicyCreatePayload, V2PolicyParameterValue, V2PolicyResponse } from '@/api/policy';
import { Policy } from '@/types/ingredients/Policy';
import { PolicyMetadata } from '@/types/metadata/policyMetadata';
import { PolicyCreationPayload } from '@/types/payloads';
import { convertParametersToPolicyJson, convertPolicyJsonToParameters } from './conversionHelpers';
import { ParameterMetadata } from '@/types/metadata';

/**
* Adapter for converting between Policy and API formats
*/
export class PolicyAdapter {
/**
* Converts PolicyMetadata from API GET response to Policy type
* Handles snake_case to camelCase conversion
* Converts V2 API response to Policy type
*/
static fromMetadata(metadata: PolicyMetadata): Policy {
static fromV2Response(response: V2PolicyResponse): Policy {
return {
id: String(metadata.id),
countryId: metadata.country_id,
apiVersion: metadata.api_version,
parameters: convertPolicyJsonToParameters(metadata.policy_json),
id: response.id,
taxBenefitModelId: response.tax_benefit_model_id,
// Note: V2 response doesn't include parameter values directly
// They would need to be fetched separately if needed
parameters: [],
};
}

/**
* Converts Policy to format for API POST request
* Note: API expects snake_case, but we handle that at the API layer
* Converts Policy to V2 API creation payload
*
* @param policy - Policy with parameters (names and values)
* @param parametersMetadata - Metadata record for name→ID lookup
* @param taxBenefitModelId - UUID of the tax benefit model
* @param name - Optional policy name (defaults to "Unnamed policy")
* @param description - Optional policy description
*/
static toCreationPayload(policy: Policy): PolicyCreationPayload {
static toV2CreationPayload(
policy: Policy,
parametersMetadata: Record<string, ParameterMetadata>,
taxBenefitModelId: string,
name?: string,
description?: string
): V2PolicyCreatePayload {
const parameterValues: V2PolicyParameterValue[] = [];

for (const param of policy.parameters || []) {
const parameterId = PolicyAdapter.getParameterIdByName(param.name, parametersMetadata);

if (!parameterId) {
console.warn(`Parameter ID not found for: ${param.name}`);
continue;
}

// Convert each value interval to a V2 parameter value
for (const interval of param.values) {
const startDate = PolicyAdapter.toISOTimestamp(interval.startDate);
// Skip if start_date would be null (shouldn't happen in practice)
if (!startDate) {
console.warn(`Invalid start date for parameter: ${param.name}`);
continue;
}

parameterValues.push({
parameter_id: parameterId,
value_json: interval.value,
start_date: startDate,
end_date: PolicyAdapter.toISOTimestamp(interval.endDate),
});
}
}

return {
data: convertParametersToPolicyJson(policy.parameters || []),
name: name || 'Unnamed policy',
description,
tax_benefit_model_id: taxBenefitModelId,
parameter_values: parameterValues,
};
}

/**
* Look up parameter ID by name from metadata
*/
private static getParameterIdByName(
paramName: string,
parametersMetadata: Record<string, ParameterMetadata>
): string | null {
// First try direct lookup by parameter path
const param = parametersMetadata[paramName];
if (param?.id) {
return param.id;
}

// Also check by the 'parameter' field which might be the path
for (const metadata of Object.values(parametersMetadata)) {
if (metadata.parameter === paramName && metadata.id) {
return metadata.id;
}
}

return null;
}

/**
* Convert date string (YYYY-MM-DD) to ISO timestamp (YYYY-MM-DDTHH:MM:SSZ)
* Returns null for "forever" dates (9999-12-31 or 2100-12-31)
*/
private static toISOTimestamp(dateStr: string): string | null {
// Treat far-future dates as "indefinite" (null in v2 API)
if (dateStr === '9999-12-31' || dateStr === '2100-12-31') {
return null;
}

// Convert YYYY-MM-DD to ISO timestamp at midnight UTC
return `${dateStr}T00:00:00Z`;
}
}
46 changes: 0 additions & 46 deletions app/src/adapters/UserPolicyAdapter.ts

This file was deleted.

1 change: 0 additions & 1 deletion app/src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export type { DatasetEntry } from './MetadataAdapter';

// User Ingredient Adapters
export { UserReportAdapter } from './UserReportAdapter';
export { UserPolicyAdapter } from './UserPolicyAdapter';
export { UserSimulationAdapter } from './UserSimulationAdapter';
export { UserHouseholdAdapter } from './UserHouseholdAdapter';
export { UserGeographicAdapter } from './UserGeographicAdapter';
Expand Down
80 changes: 65 additions & 15 deletions app/src/api/policy.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
import { BASE_URL } from '@/constants';
import { PolicyMetadata } from '@/types/metadata/policyMetadata';
import { PolicyCreationPayload } from '@/types/payloads';
import { API_V2_BASE_URL } from '@/api/v2/taxBenefitModels';

export async function fetchPolicyById(country: string, policyId: string): Promise<PolicyMetadata> {
const url = `${BASE_URL}/${country}/policy/${policyId}`;
/**
* V2 Policy parameter value - represents a single parameter change
*/
export interface V2PolicyParameterValue {
parameter_id: string; // UUID of the parameter
value_json: number | string | boolean | Record<string, unknown>;
start_date: string; // ISO timestamp (e.g., "2025-01-01T00:00:00Z")
end_date: string | null; // ISO timestamp or null for indefinite
}

/**
* V2 Policy creation payload
*/
export interface V2PolicyCreatePayload {
name: string;
description?: string;
tax_benefit_model_id: string; // UUID of the tax benefit model
parameter_values: V2PolicyParameterValue[];
}

/**
* V2 Policy response from API
*/
export interface V2PolicyResponse {
id: string;
name: string;
description: string | null;
tax_benefit_model_id: string;
created_at: string;
updated_at: string;
}

/**
* Fetch a policy by ID from v2 API
*/
export async function fetchPolicyById(policyId: string): Promise<V2PolicyResponse> {
const url = `${API_V2_BASE_URL}/policies/${policyId}`;

const res = await fetch(url, {
method: 'GET',
Expand All @@ -17,25 +50,42 @@ export async function fetchPolicyById(country: string, policyId: string): Promis
throw new Error(`Failed to fetch policy ${policyId}`);
}

const json = await res.json();

return json.result;
return res.json();
}

export async function createPolicy(
countryId: string,
data: PolicyCreationPayload
): Promise<{ result: { policy_id: string } }> {
const url = `${BASE_URL}/${countryId}/policy`;
/**
* Create a new policy via v2 API
*/
export async function createPolicy(payload: V2PolicyCreatePayload): Promise<V2PolicyResponse> {
const url = `${API_V2_BASE_URL}/policies/`;

const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
body: JSON.stringify(payload),
});

if (!res.ok) {
throw new Error('Failed to create policy');
const errorText = await res.text();
throw new Error(`Failed to create policy: ${res.status} ${errorText}`);
}

return res.json();
}

/**
* List all policies, optionally filtered by tax benefit model
*/
export async function listPolicies(taxBenefitModelId?: string): Promise<V2PolicyResponse[]> {
let url = `${API_V2_BASE_URL}/policies/`;
if (taxBenefitModelId) {
url += `?tax_benefit_model_id=${taxBenefitModelId}`;
}

const res = await fetch(url);

if (!res.ok) {
throw new Error('Failed to list policies');
}

return res.json();
Expand Down
Loading