368 lines
22 KiB
PHP
368 lines
22 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\SimulationFormDraft;
|
|
use App\Services\SimulationExecution;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
class SimulationFormDraftTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_anonymous_draft_is_encrypted_scoped_and_restorable_without_persisting_personal_data(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$key = (string) Str::uuid();
|
|
$payload = $this->payload();
|
|
$created = $this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => $key, 'X-Simulation-Token' => $token,
|
|
])->assertCreated()->assertJsonPath('data.version_no', 1);
|
|
$id = $created->json('data.id');
|
|
$stored = DB::table('simulation_form_drafts')->where('id', $id)->first();
|
|
$this->assertNotSame(json_encode($payload['personal_snapshot']), $stored->personal_snapshot);
|
|
$this->assertNotSame(json_encode($payload['economic_snapshot']), $stored->economic_snapshot);
|
|
$this->assertSame([], SimulationFormDraft::decryptSnapshot($stored->personal_snapshot));
|
|
|
|
$this->getJson('/api/v1/simulation-drafts/current', ['X-Simulation-Token' => $token])
|
|
->assertOk()->assertJsonPath('data.personal_snapshot', [])
|
|
->assertJsonPath('data.economic_snapshot.base', '1200')
|
|
->assertJsonPath('data.economic_snapshot.journey_type', 'personal');
|
|
$this->getJson('/api/v1/simulation-drafts/'.$id, ['X-Simulation-Token' => Str::random(48)])->assertNotFound();
|
|
}
|
|
|
|
public function test_anonymous_autosave_accepts_an_explicitly_empty_personal_snapshot(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$payload = $this->payload();
|
|
$payload['personal_snapshot'] = [];
|
|
|
|
$created = $this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(),
|
|
'X-Simulation-Token' => $token,
|
|
])->assertCreated()->assertJsonPath('data.version_no', 1);
|
|
|
|
$stored = DB::table('simulation_form_drafts')->where('id', $created->json('data.id'))->first();
|
|
$this->assertSame([], SimulationFormDraft::decryptSnapshot($stored->personal_snapshot));
|
|
$this->getJson('/api/v1/simulation-drafts/current', ['X-Simulation-Token' => $token])
|
|
->assertOk()->assertJsonPath('data.personal_snapshot', [])
|
|
->assertJsonPath('data.economic_snapshot.purpose', 'simulate_only');
|
|
}
|
|
|
|
public function test_exploratory_small_business_idea_and_market_assumptions_are_saved_in_the_economic_snapshot(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$payload = $this->payload();
|
|
$payload['personal_snapshot'] = [];
|
|
$payload['economic_snapshot'] = [
|
|
'journey_type' => 'small_business',
|
|
'purpose' => 'simulate_only',
|
|
'project_title' => 'Piloto sintetico sem semeadura',
|
|
'maturity_stage' => 'operation',
|
|
'selection_labels' => ['maturity_stage' => 'Operação'],
|
|
'project_problem' => 'Necessidade declarada para teste.',
|
|
'target_customers' => 'Clientes hipoteticos para teste.',
|
|
'value_proposition' => 'Proposta de valor declarada para teste.',
|
|
'market_assumption' => 'Hipotese de procura, sem fonte oficial.',
|
|
'market_source' => 'Declaracao de teste, 2026-10-04.',
|
|
'competitor_assumption' => 'Alternativas locais por verificar.',
|
|
'sales_channel_assumption' => 'Venda directa em avaliação.',
|
|
'marketing_approach' => 'Testar divulgação local.',
|
|
'location_rationale' => 'Proximidade de clientes a confirmar.',
|
|
'customer_validation_status' => 'partial',
|
|
'cost_estimate_status' => 'unknown',
|
|
'location_assessment_status' => 'partial',
|
|
'funding_status' => 'in_process',
|
|
'project_questionnaire_version' => 'investmoz-project-dossier-2026-10-05.1',
|
|
'project_risk_responses_snapshot' => json_encode([['risk_id' => 'customers', 'action' => 'Entrevistar potenciais clientes', 'owner' => 'Equipa', 'estimated_cost' => '0', 'due_date' => '2026-11-30']], JSON_THROW_ON_ERROR),
|
|
'project_milestones' => 'Planeado: validar procura; prazo: 2026-11-30.',
|
|
'project_team_snapshot' => json_encode([['competency' => 'Gestão', 'state' => 'gap', 'action' => 'Formação', 'estimated_cost' => '1200.00', 'due_date' => '2026-11-30']], JSON_THROW_ON_ERROR),
|
|
'project_execution_snapshot' => json_encode([['title' => 'Abrir loja', 'due_date' => '2026-12-01', 'status' => 'planned', 'planned_amount' => '5000', 'actual_amount' => '']], JSON_THROW_ON_ERROR),
|
|
];
|
|
|
|
$created = $this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(),
|
|
'X-Simulation-Token' => $token,
|
|
])->assertCreated();
|
|
|
|
$this->getJson('/api/v1/simulation-drafts/current', ['X-Simulation-Token' => $token])
|
|
->assertOk()
|
|
->assertJsonPath('data.personal_snapshot', [])
|
|
->assertJsonPath('data.economic_snapshot.project_problem', 'Necessidade declarada para teste.')
|
|
->assertJsonPath('data.economic_snapshot.market_source', 'Declaracao de teste, 2026-10-04.')
|
|
->assertJsonPath('data.economic_snapshot.project_questionnaire_version', 'investmoz-project-dossier-2026-10-05.1')
|
|
->assertJsonPath('data.economic_snapshot.customer_validation_status', 'partial')
|
|
->assertJsonPath('data.economic_snapshot.project_risk_responses_snapshot', $payload['economic_snapshot']['project_risk_responses_snapshot'])
|
|
->assertJsonPath('data.economic_snapshot.selection_labels.maturity_stage', 'Operação')
|
|
->assertJsonPath('data.economic_snapshot.project_milestones', 'Planeado: validar procura; prazo: 2026-11-30.')
|
|
->assertJsonPath('data.economic_snapshot.project_team_snapshot', $payload['economic_snapshot']['project_team_snapshot'])
|
|
->assertJsonPath('data.economic_snapshot.project_execution_snapshot', $payload['economic_snapshot']['project_execution_snapshot']);
|
|
}
|
|
|
|
public function test_structured_project_plans_reject_negative_amounts(): void
|
|
{
|
|
$payload = $this->payload();
|
|
$payload['economic_snapshot']['project_execution_snapshot'] = json_encode([
|
|
['title' => 'Marco', 'due_date' => '', 'status' => 'planned', 'planned_amount' => '-1', 'actual_amount' => ''],
|
|
], JSON_THROW_ON_ERROR);
|
|
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.project_execution_snapshot');
|
|
}
|
|
|
|
public function test_project_risk_responses_reject_negative_costs_and_invalid_statuses(): void
|
|
{
|
|
$payload = $this->payload();
|
|
$payload['economic_snapshot']['project_risk_responses_snapshot'] = json_encode([
|
|
['risk_id' => 'customers', 'action' => 'Validar', 'owner' => '', 'estimated_cost' => '-1', 'due_date' => ''],
|
|
], JSON_THROW_ON_ERROR);
|
|
$payload['economic_snapshot']['funding_status'] = 'guaranteed';
|
|
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertUnprocessable()->assertJsonValidationErrors([
|
|
'economic_snapshot.project_risk_responses_snapshot', 'economic_snapshot.funding_status',
|
|
]);
|
|
}
|
|
|
|
public function test_creation_is_idempotent_and_rejects_key_reuse_with_changed_data(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$key = (string) Str::uuid();
|
|
$headers = ['Idempotency-Key' => $key, 'X-Simulation-Token' => $token];
|
|
$payload = $this->payload();
|
|
$first = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated();
|
|
$id = $first->json('data.id');
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, $headers)
|
|
->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.replayed', true);
|
|
$payload['economic_snapshot']['base'] = '1300';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertConflict();
|
|
}
|
|
|
|
public function test_clients_cannot_forge_server_owned_simulation_sources(): void
|
|
{
|
|
$payload = $this->payload();
|
|
$payload['economic_snapshot']['simulation_sources'] = [['simulation_id' => (string) Str::uuid()]];
|
|
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertUnprocessable();
|
|
}
|
|
|
|
public function test_updates_require_a_current_etag_and_delete_is_reversible_until_expiry(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$headers = ['Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token];
|
|
$payload = $this->payload();
|
|
$created = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated();
|
|
$id = $created->json('data.id');
|
|
$url = '/api/v1/simulation-drafts/'.$id;
|
|
$this->putJson($url, $payload, ['X-Simulation-Token' => $token])->assertStatus(428);
|
|
$this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => '"sfd-'.$id.'-v99"'])->assertStatus(412);
|
|
$updated = $this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => $created->headers->get('ETag')])
|
|
->assertOk()->assertJsonPath('data.version_no', 2);
|
|
$expectedTokenHash = hash_hmac('sha256', $token, (string) config('app.key'));
|
|
$this->assertSame(
|
|
$expectedTokenHash,
|
|
DB::selectOne("select current_setting('app.simulation_token_hash', true) as token_hash")->token_hash,
|
|
);
|
|
$storedBeforeDelete = DB::table('simulation_form_drafts')->where('id', $id)->first();
|
|
$this->assertSame($expectedTokenHash, rtrim($storedBeforeDelete->anonymous_token_hash));
|
|
$this->assertNull($storedBeforeDelete->deleted_at);
|
|
$this->deleteJson($url, [], ['X-Simulation-Token' => $token, 'If-Match' => $updated->headers->get('ETag')])->assertNoContent();
|
|
$this->assertDatabaseHas('simulation_form_drafts', ['id' => $id]);
|
|
$this->assertDatabaseMissing('simulation_form_drafts', ['id' => $id, 'deleted_at' => null]);
|
|
$this->getJson($url, ['X-Simulation-Token' => $token])->assertNotFound();
|
|
}
|
|
|
|
public function test_completed_simulation_is_attached_with_explicit_consent_as_an_immutable_versioned_snapshot(): void
|
|
{
|
|
Queue::fake();
|
|
$token = Str::random(48);
|
|
$formalPayload = $this->payload();
|
|
$formalPayload['economic_snapshot']['purpose'] = 'own_investment';
|
|
$draftResponse = $this->postJson('/api/v1/simulation-drafts', $formalPayload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
|
|
])->assertCreated();
|
|
$draftId = $draftResponse->json('data.id');
|
|
$simulationResponse = $this->postJson('/api/v1/simulations', [
|
|
'base' => '1200', 'rate' => '8', 'reduction' => '0',
|
|
], ['Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token])->assertAccepted();
|
|
$simulationId = $simulationResponse->json('data.id');
|
|
app(SimulationExecution::class)->setContext(null, hash_hmac('sha256', $token, (string) config('app.key')));
|
|
$result = ['base' => '1200', 'rate_percent' => '8', 'reduction_percent' => '0', 'indicative_total' => '96.00000000', 'legal_interpretation' => false, 'formula_version' => 'portal-tax-arithmetic-v1'];
|
|
DB::table('simulations')->where('id', $simulationId)->update([
|
|
'status' => 'completed', 'version_no' => 2, 'result_snapshot' => json_encode($result),
|
|
]);
|
|
$canonicalResult = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [json_encode($result, JSON_THROW_ON_ERROR)])->canonical;
|
|
$reportHash = hash('sha256', $canonicalResult);
|
|
DB::table('simulation_reports')->insert([
|
|
'id' => (string) Str::uuid(), 'simulation_id' => $simulationId, 'version_no' => 1, 'status' => 'ready',
|
|
'report_snapshot' => json_encode($result), 'report_sha256' => $reportHash,
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
$url = '/api/v1/simulation-drafts/'.$draftId.'/conversions';
|
|
$headers = ['X-Simulation-Token' => $token, 'If-Match' => $draftResponse->headers->get('ETag')];
|
|
|
|
$this->postJson($url, ['simulation_id' => $simulationId, 'consent_reuse' => false], $headers)->assertUnprocessable();
|
|
$attached = $this->postJson($url, ['simulation_id' => $simulationId, 'consent_reuse' => true], $headers)
|
|
->assertOk()->assertJsonPath('replayed', false)
|
|
->assertJsonPath('data.version_no', 2)
|
|
->assertJsonPath('data.simulation_sources.0.result_snapshot.legal_interpretation', false)
|
|
->assertJsonPath('data.simulation_sources.0.report_sha256', $reportHash)
|
|
->assertJsonMissingPath('data.economic_snapshot.simulation_sources');
|
|
|
|
$this->postJson($url, ['simulation_id' => $simulationId, 'consent_reuse' => true], $headers)
|
|
->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('data.version_no', 2);
|
|
$this->assertSame(1, count($attached->json('data.simulation_sources')));
|
|
$this->assertSame(2, $attached->json('data.simulation_sources.0.source_version_no'));
|
|
$this->assertNotEmpty($attached->json('data.simulation_sources.0.consented_at'));
|
|
$this->assertFalse($attached->json('replayed'));
|
|
|
|
$formalDraft = $this->payload();
|
|
$formalDraft['economic_snapshot']['purpose'] = 'own_investment';
|
|
$formalDraft['economic_snapshot']['reuse_simulation'] = 'no';
|
|
$this->putJson('/api/v1/simulation-drafts/'.$draftId, $formalDraft, [
|
|
'X-Simulation-Token' => $token, 'If-Match' => $attached->headers->get('ETag'),
|
|
])->assertOk();
|
|
$this->getJson('/api/v1/simulation-drafts/'.$draftId, ['X-Simulation-Token' => $token])
|
|
->assertOk()->assertJsonPath('data.simulation_sources', []);
|
|
}
|
|
|
|
public function test_simulation_conversion_rejects_a_report_that_does_not_match_the_result_snapshot(): void
|
|
{
|
|
Queue::fake();
|
|
$token = Str::random(48);
|
|
$formalPayload = $this->payload();
|
|
$formalPayload['economic_snapshot']['purpose'] = 'own_investment';
|
|
$draft = $this->postJson('/api/v1/simulation-drafts', $formalPayload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
|
|
])->assertCreated();
|
|
$simulation = $this->postJson('/api/v1/simulations', [
|
|
'base' => '1200', 'rate' => '8', 'reduction' => '0',
|
|
], ['Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token])->assertAccepted();
|
|
$simulationId = $simulation->json('data.id');
|
|
app(SimulationExecution::class)->setContext(null, hash_hmac('sha256', $token, (string) config('app.key')));
|
|
$result = ['indicative_total' => '96.00000000', 'legal_interpretation' => false, 'formula_version' => 'portal-tax-arithmetic-v1'];
|
|
$forgedReport = $result;
|
|
$forgedReport['indicative_total'] = '96000.00000000';
|
|
DB::table('simulations')->where('id', $simulationId)->update([
|
|
'status' => 'completed', 'version_no' => 2, 'result_snapshot' => json_encode($result),
|
|
]);
|
|
$canonicalReport = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [json_encode($forgedReport, JSON_THROW_ON_ERROR)])->canonical;
|
|
DB::table('simulation_reports')->insert([
|
|
'id' => (string) Str::uuid(), 'simulation_id' => $simulationId, 'version_no' => 1, 'status' => 'ready',
|
|
'report_snapshot' => json_encode($forgedReport), 'report_sha256' => hash('sha256', $canonicalReport),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
$this->postJson('/api/v1/simulation-drafts/'.$draft->json('data.id').'/conversions', [
|
|
'simulation_id' => $simulationId, 'consent_reuse' => true,
|
|
], ['X-Simulation-Token' => $token, 'If-Match' => $draft->headers->get('ETag')])
|
|
->assertStatus(409);
|
|
|
|
DB::table('simulation_reports')->insert([
|
|
'id' => (string) Str::uuid(), 'simulation_id' => $simulationId, 'version_no' => 2, 'status' => 'ready',
|
|
'report_snapshot' => json_encode($result), 'report_sha256' => str_repeat('0', 64),
|
|
'created_at' => now()->addSecond(), 'updated_at' => now()->addSecond(),
|
|
]);
|
|
$this->postJson('/api/v1/simulation-drafts/'.$draft->json('data.id').'/conversions', [
|
|
'simulation_id' => $simulationId, 'consent_reuse' => true,
|
|
], ['X-Simulation-Token' => $token, 'If-Match' => $draft->headers->get('ETag')])
|
|
->assertStatus(409);
|
|
}
|
|
|
|
public function test_simulation_conversion_rejects_an_unowned_execution_token(): void
|
|
{
|
|
Queue::fake();
|
|
$draftToken = Str::random(48);
|
|
$draft = $this->postJson('/api/v1/simulation-drafts', $this->payload(), [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $draftToken,
|
|
])->assertCreated();
|
|
$otherToken = Str::random(48);
|
|
$simulation = $this->postJson('/api/v1/simulations', ['base' => '1200', 'rate' => '8', 'reduction' => '0'], [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $otherToken,
|
|
])->assertAccepted();
|
|
|
|
$this->postJson('/api/v1/simulation-drafts/'.$draft->json('data.id').'/conversions', [
|
|
'simulation_id' => $simulation->json('data.id'), 'consent_reuse' => true,
|
|
], ['X-Simulation-Token' => $draftToken, 'If-Match' => $draft->headers->get('ETag')])->assertNotFound();
|
|
}
|
|
|
|
public function test_server_rejects_invalid_rates_and_unlisted_snapshot_fields(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$payload = $this->payload();
|
|
$payload['economic_snapshot']['rate'] = '101';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
|
|
])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.rate');
|
|
|
|
$payload = $this->payload();
|
|
$payload['economic_snapshot']['unapproved_field'] = 'not accepted';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
|
|
])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot');
|
|
}
|
|
|
|
public function test_fiscal_mode_rejects_manual_rates_and_incomplete_custom_assumptions(): void
|
|
{
|
|
$token = Str::random(48);
|
|
$payload = $this->payload();
|
|
unset($payload['economic_snapshot']['tax_calculation'], $payload['economic_snapshot']['base'], $payload['economic_snapshot']['rate'], $payload['economic_snapshot']['reduction']);
|
|
$payload['economic_snapshot']['fiscal_scenario_mode'] = 'approved_parameters';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
|
|
])->assertCreated();
|
|
|
|
$payload['economic_snapshot']['base'] = '1000';
|
|
$payload['economic_snapshot']['rate'] = '5';
|
|
$payload['economic_snapshot']['reduction'] = '0';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertUnprocessable()->assertJsonValidationErrors(['economic_snapshot.base', 'economic_snapshot.rate', 'economic_snapshot.reduction']);
|
|
|
|
$payload = $this->payload();
|
|
unset($payload['economic_snapshot']['tax_calculation']);
|
|
$payload['economic_snapshot']['fiscal_scenario_mode'] = 'custom_planning';
|
|
unset($payload['economic_snapshot']['base'], $payload['economic_snapshot']['rate'], $payload['economic_snapshot']['reduction']);
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertCreated();
|
|
|
|
$payload = $this->payload();
|
|
unset($payload['economic_snapshot']['tax_calculation'], $payload['economic_snapshot']['base'], $payload['economic_snapshot']['reduction']);
|
|
$payload['economic_snapshot']['fiscal_scenario_mode'] = 'custom_planning';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertUnprocessable()->assertJsonValidationErrors(['economic_snapshot.base', 'economic_snapshot.reduction']);
|
|
|
|
$payload = $this->payload();
|
|
unset($payload['economic_snapshot']['tax_calculation']);
|
|
$payload['economic_snapshot']['fiscal_scenario_mode'] = 'custom_planning';
|
|
$payload['economic_snapshot']['base'] = '1000';
|
|
$payload['economic_snapshot']['rate'] = '5';
|
|
$payload['economic_snapshot']['reduction'] = '0';
|
|
$this->postJson('/api/v1/simulation-drafts', $payload, [
|
|
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
|
|
])->assertCreated();
|
|
}
|
|
|
|
/** @return array{personal_snapshot: array<string,string>, economic_snapshot: array<string,string>} */
|
|
private function payload(): array
|
|
{
|
|
return [
|
|
'personal_snapshot' => ['legal_name' => 'Entidade de teste', 'email' => 'teste@example.com'],
|
|
'economic_snapshot' => [
|
|
'journey_type' => 'personal', 'detail_level' => 'simple', 'business_plan_snapshot' => '{"version":1}',
|
|
'purpose' => 'simulate_only', 'tax_calculation' => 'yes', 'base' => '1200', 'rate' => '8', 'reduction' => '0',
|
|
],
|
|
];
|
|
}
|
|
}
|