Add unit tests for superuser invoices route wiring, tabs, and API contracts; enhance calculations and logic in InvoicingBillingPeriodStatistics.vue; update OpenAPI schema with WeatherAPI and fixed pricing distribution functionality.
This commit is contained in:
+208
@@ -4932,6 +4932,10 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Fixed pricing distribution retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionResponse'
|
||||
|
||||
/superuser/invoicing/period/distribution/wash-subscriptions:
|
||||
get:
|
||||
@@ -6488,6 +6492,10 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Current weather retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WeatherApiObjectResponse'
|
||||
|
||||
/modules/weatherapi/forecast:
|
||||
get:
|
||||
@@ -6514,6 +6522,10 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Forecast weather retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WeatherApiObjectResponse'
|
||||
|
||||
/modules/weatherapi/search:
|
||||
get:
|
||||
@@ -6532,6 +6544,10 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Location search results retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/WeatherApiObjectResponse'
|
||||
|
||||
/departments/weather:
|
||||
get:
|
||||
@@ -6562,6 +6578,7 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentWeatherTimelineEntry'
|
||||
$ref: '#/components/schemas/DepartmentWeatherTimelineResponse'
|
||||
|
||||
/modules/entra/users:
|
||||
get:
|
||||
@@ -8332,6 +8349,27 @@ components:
|
||||
$ref: '#/components/schemas/DepartmentWeatherStatus'
|
||||
required: [time, weather, washes, hours, status]
|
||||
|
||||
WeatherApiObjectResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
required: [data]
|
||||
|
||||
DepartmentWeatherTimelineResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ModuleConfigEnvelopeBase'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentWeatherTimelineEntry'
|
||||
required: [data]
|
||||
|
||||
ModuleConfigEntry:
|
||||
type: object
|
||||
properties:
|
||||
@@ -8771,6 +8809,176 @@ components:
|
||||
- internal_total
|
||||
- order_ids
|
||||
|
||||
InvoicingFixedPricingDistributionResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionCustomer'
|
||||
meta:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionMeta'
|
||||
includes:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionIncludes'
|
||||
required:
|
||||
- success
|
||||
- data
|
||||
- meta
|
||||
- includes
|
||||
|
||||
InvoicingFixedPricingDistributionCustomer:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
customer_number:
|
||||
type: integer
|
||||
customer_name:
|
||||
type: string
|
||||
transactions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionTransaction'
|
||||
requires_action:
|
||||
type: boolean
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
fixed_pricing:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionFixedPricing'
|
||||
required:
|
||||
- fixed_pricing
|
||||
required:
|
||||
- id
|
||||
- customer_number
|
||||
- customer_name
|
||||
- transactions
|
||||
- requires_action
|
||||
- meta
|
||||
|
||||
InvoicingFixedPricingDistributionTransaction:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
date:
|
||||
type: string
|
||||
description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
|
||||
example: '2026-02-02 10:43:41'
|
||||
amount:
|
||||
type: number
|
||||
booked:
|
||||
type: boolean
|
||||
excluded:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- date
|
||||
- amount
|
||||
- booked
|
||||
- excluded
|
||||
|
||||
InvoicingFixedPricingDistributionFixedPricing:
|
||||
type: object
|
||||
properties:
|
||||
customer_number:
|
||||
type: integer
|
||||
price:
|
||||
type: number
|
||||
description:
|
||||
type: string
|
||||
original_price:
|
||||
type: number
|
||||
department_totals:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
|
||||
department_totals_relative:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
|
||||
required:
|
||||
- customer_number
|
||||
- price
|
||||
- description
|
||||
- original_price
|
||||
- department_totals
|
||||
- department_totals_relative
|
||||
|
||||
InvoicingFixedPricingDistributionMeta:
|
||||
type: object
|
||||
properties:
|
||||
date_from:
|
||||
type: string
|
||||
description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
|
||||
example: '2026-02-01 00:00:00'
|
||||
date_to:
|
||||
type: string
|
||||
description: Datetime in `YYYY-MM-DD HH:mm:ss` format.
|
||||
example: '2026-02-28 23:59:59'
|
||||
required:
|
||||
- date_from
|
||||
- date_to
|
||||
|
||||
InvoicingFixedPricingDistributionIncludes:
|
||||
type: object
|
||||
properties:
|
||||
debug_invoicing_period_customers_with_orders_in_date_range:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
|
||||
debug_invoicing_period_process_customer_numbers:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
|
||||
debug_invoicing_period_get_transactions_for_customers_in_date_range:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
|
||||
debug_invoicing_period_calculate_transaction_totals:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
|
||||
debug_invoicing_period_construct_customer_objects:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime'
|
||||
collective_fixed_pricing_results:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionCollectiveResults'
|
||||
additionalProperties: true
|
||||
|
||||
InvoicingFixedPricingDistributionExecutionTime:
|
||||
type: object
|
||||
properties:
|
||||
execution_time:
|
||||
type: number
|
||||
required:
|
||||
- execution_time
|
||||
|
||||
InvoicingFixedPricingDistributionCollectiveResults:
|
||||
type: object
|
||||
properties:
|
||||
total_fixed_price:
|
||||
type: number
|
||||
total_original_price:
|
||||
type: number
|
||||
total_department_totals:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
|
||||
total_department_totals_relative:
|
||||
$ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray'
|
||||
total_department_totals_parsed:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
total_department_totals_relative_parsed:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
required:
|
||||
- total_fixed_price
|
||||
- total_original_price
|
||||
- total_department_totals
|
||||
- total_department_totals_relative
|
||||
- total_department_totals_parsed
|
||||
- total_department_totals_relative_parsed
|
||||
|
||||
InvoicingFixedPricingDistributionNumberMapOrEmptyArray:
|
||||
oneOf:
|
||||
- type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
- type: array
|
||||
maxItems: 0
|
||||
|
||||
SelfServeLaneStatus:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+44
-7
@@ -143,6 +143,35 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const getDepartmentRelativeBookedAmount = (department: any, distribution: any) => {
|
||||
if (distribution.total_fixed_price === 0) {
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* This takes the total relative booked amount for each department, and then calculates the relative booked amount for the department 75.
|
||||
* Meaning if:
|
||||
* department1 = 1000
|
||||
* department2 = 2000
|
||||
* total booked amount = 6000
|
||||
* Then department1 relative booked amount = (1000 / 3000) * 6000 = 2000
|
||||
* and department2 relative booked amount = (2000 / 3000) * 6000 = 4000
|
||||
*/
|
||||
const department_original_price = distribution.total_department_totals[department.id] || 0;
|
||||
const total_original_price = distribution.total_original_price || 0;
|
||||
const total_booked_amount = view.computed.currentViewTotalNetAmountBooked.value || 0;
|
||||
const relative_booked_amount = (department_original_price / total_original_price) * total_booked_amount;
|
||||
return Math.round(relative_booked_amount);
|
||||
//return SessionUser.functions.currency.toLocal(Math.round(relative_booked_amount));
|
||||
};
|
||||
|
||||
const getTotalAmountDistributed = (distribution: { total_department_totals_parsed: { [s: string]: unknown; } | ArrayLike<unknown>; total_original_price: number; }) => {
|
||||
const departments_original_price = Object.values(distribution.total_department_totals_parsed).reduce((acc, value) => acc + value, 0);
|
||||
const total_original_price = distribution.total_original_price || 0;
|
||||
const total_booked_amount = view.computed.currentViewTotalNetAmountBooked.value || 0;
|
||||
const total_amount_distributed = (departments_original_price / total_original_price) * total_booked_amount;
|
||||
return SessionUser.functions.currency.toLocal(Math.round(total_amount_distributed));
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -335,7 +364,8 @@ watch(
|
||||
<th>Original pris</th>
|
||||
<th>Faktisk pris</th>
|
||||
<th>Procent betalt ud fra original pris</th>
|
||||
<th>Bogført beløb <b-tooltip label="Estimeret ud fra faktisk pris i forhold til procent betalt og det totale bogførte beløb for perioden (ikke nødvendigvis det totale faktiske beløb for periode)" type="is-info">?</b-tooltip></th>
|
||||
<th>Procent tilsvarende bogført beløb</th>
|
||||
<th>Bogført beløb <b-tooltip label="Estimeret ud fra fast pris i forhold til den totale faste pris for perioden og det totale bogførte beløb for perioden (ikke nødvendigvis det totale faste pris for periode)" type="is-info">?</b-tooltip></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -345,8 +375,9 @@ watch(
|
||||
<td>{{ SessionUser.functions.currency.toLocal(Math.round(fixed_pricing_department_distribution.total_department_totals_parsed[department.name] || 0)) }}</td>
|
||||
<td>{{ SessionUser.functions.currency.toLocal(Math.round(fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0)) }}</td>
|
||||
<td>{{ getPercentageOfTotal(fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0, fixed_pricing_department_distribution.total_department_totals_parsed[department.name] || 0) }}%</td>
|
||||
<!-- view.computed.currentViewTotalNetAmountBooked.value is the total booked amount for the entire period -->
|
||||
<td>{{ SessionUser.functions.currency.toLocal(Math.round(((fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0) / (fixed_pricing_department_distribution.total_department_totals_parsed[department.name] || 0)) * view.computed.currentViewTotalNetAmountBooked.value)) }}</td>
|
||||
<td>{{ getPercentageOfTotal(fixed_pricing_department_distribution.total_department_totals[department.id] || 0, fixed_pricing_department_distribution.total_original_price) }}%</td>
|
||||
<td>{{ getDepartmentRelativeBookedAmount(department, fixed_pricing_department_distribution) }}</td>
|
||||
<!-- <td>{{ SessionUser.functions.currency.toLocal(view.computed.currentViewTotalNetAmountBooked.value * ((fixed_pricing_department_distribution.total_department_totals_relative_parsed[department.name] || 0) / fixed_pricing_department_distribution.total_fixed_price)) }}</td> -->
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<!-- Loader -->
|
||||
@@ -355,6 +386,7 @@ watch(
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Total row -->
|
||||
@@ -363,11 +395,12 @@ watch(
|
||||
<td><strong class="has-text-strikethrough">{{ SessionUser.functions.currency.toLocal(fixed_pricing_department_distribution.total_original_price) }}</strong></td>
|
||||
<td><strong>{{ SessionUser.functions.currency.toLocal(fixed_pricing_department_distribution.total_fixed_price) }}</strong></td>
|
||||
<td><strong>{{ getPercentageOfTotal(fixed_pricing_department_distribution.total_fixed_price, fixed_pricing_department_distribution.total_original_price) }}%</strong></td>
|
||||
<td><strong>{{ SessionUser.functions.currency.toLocal(view.computed.currentViewTotalNetAmountBooked.value) }}</strong></td>
|
||||
<td><strong>100%</strong></td>
|
||||
<td><strong>{{ view.computed.currentViewTotalNetAmountBooked.value }} (Distributed: {{ getTotalAmountDistributed(fixed_pricing_department_distribution) }})</strong></td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td colspan="5" class="has-text-centered">
|
||||
<td colspan="6" class="has-text-centered">
|
||||
<span class="icon is-large">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
</span>
|
||||
@@ -413,7 +446,8 @@ watch(
|
||||
<tr>
|
||||
<th>Afdeling</th>
|
||||
<th>Abonnementspris</th>
|
||||
<th>Procent af total</th>
|
||||
<th>Procent af total abonnementspris</th>
|
||||
<th>Bogført beløb <b-tooltip label="Estimeret ud fra abonnementspris i forhold til det totale abonnementsbeløb for perioden og det totale bogførte beløb for perioden (ikke nødvendigvis det totale abonnementsbeløb for periode)" type="is-info">?</b-tooltip></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -422,12 +456,14 @@ watch(
|
||||
<td>{{ department.name }}</td>
|
||||
<td>{{ SessionUser.functions.currency.toLocal(Math.round(vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed[department.name])) }}</td>
|
||||
<td>{{ getPercentageOfTotal(vehicleSubscriptionDistribution.subscription_price_department_distribution_parsed[department.name], vehicleSubscriptionDistribution.total_subscription_price) }}%</td>
|
||||
<td>{{ getDepartmentRelativeBookedAmount(department, vehicleSubscriptionDistribution) }}</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<!-- Loader -->
|
||||
<td>{{ department.name }}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- Total row -->
|
||||
@@ -435,10 +471,11 @@ watch(
|
||||
<td><strong>Total</strong></td>
|
||||
<td><strong>{{ SessionUser.functions.currency.toLocal(vehicleSubscriptionDistribution.total_subscription_price) }}</strong></td>
|
||||
<td><strong>100%</strong></td>
|
||||
<td><strong>{{ SessionUser.functions.currency.toLocal(view.computed.currentViewTotalNetAmountBooked.value) }}</strong></td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td colspan="3" class="has-text-centered">
|
||||
<td colspan="4" class="has-text-centered">
|
||||
<span class="icon is-large">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const root = process.cwd();
|
||||
|
||||
const routerSource = readFileSync(join(root, "src/router.js"), "utf8");
|
||||
const invoicesViewSource = readFileSync(
|
||||
join(root, "src/views/dashboards/superUserDashboard/CollectedOrderInvoices.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const overviewSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/collectedOrderInvoices/CollectedOrderInvoicesOverview.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const overviewEconomicSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/collectedOrderInvoices/processors/InvoicesOverviewEconomic.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const overviewBoxSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/collectedOrderInvoices/InvoiceProcessorOverviewBox.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const listPaginationSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/components/displays/pagination/models/SuperUserDashboard/CollectedOrderInvoicesListPagination.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/InvoicingBillingPeriod.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodTopSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Top.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodLeftSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Left.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodRightSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Right.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodDateSelectorSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/InvoicingBillingPeriodDatePeriodSelector.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
const periodViewImportSource = readFileSync(
|
||||
join(
|
||||
root,
|
||||
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue"
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
describe("superuser invoices route wiring", () => {
|
||||
it("maps /superuser/invoices to CollectedOrderInvoices view", () => {
|
||||
expect(routerSource).toContain("name: 'collectedorderinvoices'");
|
||||
expect(routerSource).toContain("path: '/superuser/invoices'");
|
||||
expect(routerSource).toContain("component: CollectedOrderInvoices");
|
||||
});
|
||||
|
||||
it("keeps details route for a single collected invoice", () => {
|
||||
expect(routerSource).toContain("name: 'collectedorderinvoice'");
|
||||
expect(routerSource).toContain("path: '/superuser/invoices/:collectedOrderInvoiceId'");
|
||||
expect(routerSource).toContain("component: CollectedOrderInvoice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CollectedOrderInvoices tab container contract", () => {
|
||||
it("defines all expected top-level tabs", () => {
|
||||
expect(invoicesViewSource).toContain("{ name: 'Overblik', slot: 'overview'");
|
||||
expect(invoicesViewSource).toContain("{ name: 'Fakturaer', slot: 'invoices'");
|
||||
expect(invoicesViewSource).toContain("{ name: 'Periode', slot: 'period'");
|
||||
});
|
||||
|
||||
it("keeps active tab synced with route query", () => {
|
||||
expect(invoicesViewSource).toContain("watch(() => router.currentRoute.value.query.activeTab");
|
||||
expect(invoicesViewSource).toContain("activeTab.value = newTab;");
|
||||
expect(invoicesViewSource).toContain("watch(activeTab, (newTab) => {");
|
||||
expect(invoicesViewSource).toContain("router.replace({ query: { ...router.currentRoute.value.query, activeTab: newTab } });");
|
||||
});
|
||||
|
||||
it("defaults to overview when route query is missing or invalid", () => {
|
||||
expect(invoicesViewSource).toContain("const activeTab = ref('overview');");
|
||||
expect(invoicesViewSource).toContain("activeTab.value = 'overview';");
|
||||
});
|
||||
|
||||
it("maps each tab slot to the expected component", () => {
|
||||
expect(invoicesViewSource).toContain("case 'overview':");
|
||||
expect(invoicesViewSource).toContain("return CollectedOrderInvoicesOverview;");
|
||||
expect(invoicesViewSource).toContain("case 'invoices':");
|
||||
expect(invoicesViewSource).toContain("return CollectedOrderInvoicesListPagination;");
|
||||
expect(invoicesViewSource).toContain("case 'period':");
|
||||
expect(invoicesViewSource).toContain("return InvoicingBillingPeriod;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Overview tab contract", () => {
|
||||
it("renders economic overview processor", () => {
|
||||
expect(overviewSource).toContain("<invoices-overview-economic />");
|
||||
});
|
||||
|
||||
it("loads economic stats via session request", () => {
|
||||
expect(overviewEconomicSource).toContain("SessionUser.request(");
|
||||
expect(overviewEconomicSource).toContain("'/collected-invoices/economic/overview'");
|
||||
expect(overviewEconomicSource).toContain("'GET'");
|
||||
});
|
||||
|
||||
it("updates all key counters from API response", () => {
|
||||
expect(overviewEconomicSource).toContain("total.value = result.total;");
|
||||
expect(overviewEconomicSource).toContain("paid.value = result.paid;");
|
||||
expect(overviewEconomicSource).toContain("unpaid.value = result.unpaid;");
|
||||
expect(overviewEconomicSource).toContain("overdue.value = result.overdue;");
|
||||
expect(overviewEconomicSource).toContain("booked.value = result.booked;");
|
||||
expect(overviewEconomicSource).toContain("draft.value = result.draft;");
|
||||
expect(overviewEconomicSource).toContain("sent.value = result.sent;");
|
||||
expect(overviewEconomicSource).toContain("error.value = result.error;");
|
||||
});
|
||||
|
||||
it("keeps both overview actions wired to refresh after execution", () => {
|
||||
expect(overviewEconomicSource).toContain("runCheckDrafts()");
|
||||
expect(overviewEconomicSource).toContain("runCheckErrors()");
|
||||
expect(overviewEconomicSource).toContain("refresh();");
|
||||
});
|
||||
|
||||
it("supports error lifecycle and dismiss interaction", () => {
|
||||
expect(overviewEconomicSource).toContain("const addErrorMessage =");
|
||||
expect(overviewEconomicSource).toContain("const removeErrorMessage =");
|
||||
expect(overviewEconomicSource).toContain("const clearErrorMessages =");
|
||||
expect(overviewEconomicSource).toContain("const hasErrorMessages =");
|
||||
expect(overviewEconomicSource).toContain('<button class="delete" @click="removeErrorMessage(index)"></button>');
|
||||
});
|
||||
|
||||
it("keeps refresh propagation through overview card boxes", () => {
|
||||
expect(overviewEconomicSource).toContain("<InvoiceProcessorOverviewBox");
|
||||
expect(overviewEconomicSource).toContain("@refresh=\"refresh\"");
|
||||
});
|
||||
});
|
||||
|
||||
describe("InvoiceProcessorOverviewBox contract", () => {
|
||||
it("requires options/count/loading props and optional onClick handler", () => {
|
||||
expect(overviewBoxSource).toContain("options:");
|
||||
expect(overviewBoxSource).toContain("count:");
|
||||
expect(overviewBoxSource).toContain("loading:");
|
||||
expect(overviewBoxSource).toContain("onClick:");
|
||||
});
|
||||
|
||||
it("shows spinner while loading and emits refresh otherwise", () => {
|
||||
expect(overviewBoxSource).toContain("v-if=\"loading\"");
|
||||
expect(overviewBoxSource).toContain("fa-spinner fa-spin");
|
||||
expect(overviewBoxSource).toContain("fa-sync-alt");
|
||||
expect(overviewBoxSource).toContain("@click=\"$emit('refresh')\"");
|
||||
});
|
||||
|
||||
it("shows View action only when click callback exists", () => {
|
||||
expect(overviewBoxSource).toContain("const hasClickAction = () => {");
|
||||
expect(overviewBoxSource).toContain("v-if=\"hasClickAction()\"");
|
||||
expect(overviewBoxSource).toContain("View");
|
||||
expect(overviewBoxSource).toContain("v-if=\"!hasClickAction()\"");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fakturaer tab contract", () => {
|
||||
it("configures endpoint, sorting, filters, and optional auto-load", () => {
|
||||
expect(listPaginationSource).toContain('setEndpoint("/collected-invoices", false);');
|
||||
expect(listPaginationSource).toContain('setEndpoint("/collected-invoices/ready-to-invoice", false);');
|
||||
expect(listPaginationSource).toContain('setOrder("closed_at", "asc", false);');
|
||||
expect(listPaginationSource).toContain("setHideSearchField(props.hideSearch);");
|
||||
expect(listPaginationSource).toContain('setFilter("customer_number", props.setCustomerFilter, false);');
|
||||
expect(listPaginationSource).toContain('setFilter("closed_at", \'is_null\', false);');
|
||||
expect(listPaginationSource).toContain('setFilter("booked_invoice_id", \'is_null\', false);');
|
||||
expect(listPaginationSource).toContain("if (props.autoLoad) {");
|
||||
expect(listPaginationSource).toContain("loadList();");
|
||||
});
|
||||
|
||||
it("supports mass compare flow across all pages with auth and ordering", () => {
|
||||
expect(listPaginationSource).toContain("const fetchAllPages = async () => {");
|
||||
expect(listPaginationSource).toContain("const totalPages = Math.ceil(metaTotalItems.value / metaItemsPerPage.value);");
|
||||
expect(listPaginationSource).toContain("const token = localStorage.getItem('token');");
|
||||
expect(listPaginationSource).toContain("if (totalPages > 20) {");
|
||||
expect(listPaginationSource).toContain("axios.get(API_URL + endpoint.value");
|
||||
expect(listPaginationSource).toContain("Authorization: `Bearer ${token}`");
|
||||
expect(listPaginationSource).toContain("order: `${orderBy.value}:${orderDirection.value}`");
|
||||
});
|
||||
|
||||
it("keeps compare modal workflow and button safeguards", () => {
|
||||
expect(listPaginationSource).toContain("initCompareDataStructure();");
|
||||
expect(listPaginationSource).toContain("clearProgress();");
|
||||
expect(listPaginationSource).toContain("setTitle('Sammenlign fakturaer med E-conomic');");
|
||||
expect(listPaginationSource).toContain("getParsedDataFormatFunction()(allInvoices);");
|
||||
expect(listPaginationSource).toContain("await showMassDataInsertModal();");
|
||||
expect(listPaginationSource).toContain("Sammenlign alle med E-conomic");
|
||||
expect(listPaginationSource).toContain(":disabled=\"isMassComparing || isLoading || !list || list.length === 0\"");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Periode tab contract", () => {
|
||||
it("assembles top/left/right/bottom sections in the billing-period layout", () => {
|
||||
expect(periodSource).toContain("<InvoicingBillingPeriodLayoutDefault>");
|
||||
expect(periodSource).toContain("<template #content-top>");
|
||||
expect(periodSource).toContain("<template #content-left>");
|
||||
expect(periodSource).toContain("<template #content-right>");
|
||||
expect(periodSource).toContain("<template #content-bottom>");
|
||||
});
|
||||
|
||||
it("keeps top selector and left dynamic view renderer", () => {
|
||||
expect(periodTopSource).toContain("<InvoicingBillingPeriodDatePeriodSelector/>");
|
||||
expect(periodLeftSource).toContain('<Component v-bind:is="view.computed.currentViewComponent.value" />');
|
||||
});
|
||||
|
||||
it("fetches period dataset and refreshes when date range changes", () => {
|
||||
expect(periodRightSource).toContain("SessionUser.request(");
|
||||
expect(periodRightSource).toContain("'/superuser/invoicing/period'");
|
||||
expect(periodRightSource).toContain("dateFrom: dates.variables.start.value.toISOString().split('T')[0]");
|
||||
expect(periodRightSource).toContain("dateTo: dates.variables.end.value.toISOString().split('T')[0]");
|
||||
expect(periodRightSource).toContain("watch(() => dates.variables.start.value, () => {");
|
||||
expect(periodRightSource).toContain("getPeriod();");
|
||||
});
|
||||
|
||||
it("exposes all right-panel period views and click switching", () => {
|
||||
expect(periodRightSource).toContain("view: 'all'");
|
||||
expect(periodRightSource).toContain("view: 'invoice_per_order'");
|
||||
expect(periodRightSource).toContain("view: 'fixed_pricing'");
|
||||
expect(periodRightSource).toContain("view: 'tank_cleaning'");
|
||||
expect(periodRightSource).toContain("view: 'special_arrangements'");
|
||||
expect(periodRightSource).toContain("view: 'vehicle_subscriptions'");
|
||||
expect(periodRightSource).toContain("view: 'possible_duplicates'");
|
||||
expect(periodRightSource).toContain("view.variables.currentView.value = type.view;");
|
||||
});
|
||||
|
||||
it("keeps date selector update action and auto change emission", () => {
|
||||
expect(periodDateSelectorSource).toContain("dates.functions.setSelection(startDate, endDate);");
|
||||
expect(periodDateSelectorSource).toContain('v-bind:auto-emit-change="true"');
|
||||
expect(periodDateSelectorSource).toContain(":class=\"{'is-loading': isReloadAnimationActive}\"");
|
||||
expect(periodDateSelectorSource).toContain("@click=\"onSelectionChange(dates.variables.start.value, dates.variables.end.value); showReloadAnimation();\"");
|
||||
});
|
||||
|
||||
it("keeps import-view mapping for all period view keys", () => {
|
||||
expect(periodViewImportSource).toContain("home: InvoicingBillingPeriodViewHome");
|
||||
expect(periodViewImportSource).toContain("fixed_pricing: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("vehicle_subscriptions: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("tank_cleaning: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("all: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("special_arrangements: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("invoice_per_order: InvoicingBillingPeriodViewAll");
|
||||
expect(periodViewImportSource).toContain("possible_duplicates: InvoicingBillingPeriodViewAll");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user