fix(companies): align optional numeric fields with API validation
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
Make employee_count, annual_revenue, and founded_year truly optional in the create company form and payload. Add field validation docs and Cypress e2e coverage for dashboard and notifications. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const apiResponse = (data: Record<string, unknown>) => ({
|
||||
success: true,
|
||||
message: "ok",
|
||||
data,
|
||||
});
|
||||
|
||||
const visitDashboard = () => {
|
||||
cy.visit("/", {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, "matchMedia").callsFake((query: string) => ({
|
||||
matches: query === "(prefers-reduced-motion: reduce)",
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: () => undefined,
|
||||
removeEventListener: () => undefined,
|
||||
addListener: () => undefined,
|
||||
removeListener: () => undefined,
|
||||
dispatchEvent: () => false,
|
||||
}));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const stubDashboardRequests = () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/summary**", (req) => {
|
||||
expect(req.query.closing_window).to.be.undefined;
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
total_tenders: 120,
|
||||
active_tenders: 72,
|
||||
awarded_tenders: 18,
|
||||
expired_tenders: 20,
|
||||
cancelled_tenders: 10,
|
||||
closing_soon: 12,
|
||||
total_estimated_value: 2500000,
|
||||
value_currency: "EUR",
|
||||
generated_at: now,
|
||||
}),
|
||||
});
|
||||
}).as("dashboardSummary");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/trend**", (req) => {
|
||||
expect(req.query).to.include({ days: "14", metric: "created" });
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
metric: "created",
|
||||
days: 14,
|
||||
series: [
|
||||
{ date: "2026-06-07", count: 4 },
|
||||
{ date: "2026-06-08", count: 7 },
|
||||
{ date: "2026-06-09", count: 9 },
|
||||
],
|
||||
}),
|
||||
});
|
||||
}).as("dashboardTrend");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/countries**", (req) => {
|
||||
expect(req.query.limit).to.eq("6");
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
total: 120,
|
||||
items: [
|
||||
{ country_code: "DE", count: 60 },
|
||||
{ country_code: "FR", count: 30 },
|
||||
{ country_code: "NL", count: 10 },
|
||||
],
|
||||
other_count: 20,
|
||||
}),
|
||||
});
|
||||
}).as("dashboardCountries");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/notice-types**", {
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
total: 100,
|
||||
items: [
|
||||
{ type: "CONTRACT_NOTICE", count: 70 },
|
||||
{ type: "AWARD_NOTICE", count: 30 },
|
||||
],
|
||||
}),
|
||||
}).as("dashboardNoticeTypes");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/closing-soon**", (req) => {
|
||||
expect(req.query.limit).to.eq("5");
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
items: [
|
||||
{
|
||||
id: "closing-tender-1",
|
||||
title: "Railway signaling upgrade",
|
||||
country_code: "DE",
|
||||
buyer_name: "Federal Rail Agency",
|
||||
submission_deadline: now + 36 * 60 * 60,
|
||||
status: "active",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
}).as("dashboardClosingSoon");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/tenders**", (req) => {
|
||||
expect(req.query).to.include({
|
||||
sort_by: "created_at",
|
||||
sort_order: "desc",
|
||||
offset: "0",
|
||||
limit: "5",
|
||||
});
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
tenders: [
|
||||
{
|
||||
id: "recent-tender-1",
|
||||
title: "Hospital equipment framework",
|
||||
country_code: "FR",
|
||||
status: "active",
|
||||
created_at: now - 60 * 60,
|
||||
buyer_organization: { name: "Central Health Buyer" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
}).as("recentTenders");
|
||||
|
||||
cy.intercept("GET", "**/api/proxy/dashboard/statistics**", (req) => {
|
||||
const days = Number(req.query.days);
|
||||
req.alias = days === 30 ? "statistics30Days" : "statistics14Days";
|
||||
req.reply({
|
||||
statusCode: 200,
|
||||
body: apiResponse({
|
||||
days,
|
||||
generated_at: now,
|
||||
daily: {
|
||||
scraped_ted: [
|
||||
{ date: "2026-06-08", count: days === 30 ? 12 : 5 },
|
||||
{ date: "2026-06-09", count: days === 30 ? 18 : 8 },
|
||||
],
|
||||
scraped_documents: [
|
||||
{ date: "2026-06-08", count: days === 30 ? 40 : 20 },
|
||||
{ date: "2026-06-09", count: days === 30 ? 60 : 30 },
|
||||
],
|
||||
translated_notices: [
|
||||
{ date: "2026-06-08", count: days === 30 ? 7 : 3 },
|
||||
{ date: "2026-06-09", count: days === 30 ? 11 : 6 },
|
||||
],
|
||||
},
|
||||
totals: {
|
||||
scraped_documents: days === 30 ? 9876 : 4321,
|
||||
translated_tenders: days === 30 ? 654 : 321,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("tender dashboard", () => {
|
||||
beforeEach(() => {
|
||||
cy.setAuthCookies();
|
||||
stubDashboardRequests();
|
||||
});
|
||||
|
||||
it("renders the new dashboard sections with API data and detail links", () => {
|
||||
visitDashboard();
|
||||
|
||||
cy.wait([
|
||||
"@dashboardSummary",
|
||||
"@dashboardTrend",
|
||||
"@dashboardCountries",
|
||||
"@dashboardNoticeTypes",
|
||||
"@dashboardClosingSoon",
|
||||
"@recentTenders",
|
||||
"@statistics14Days",
|
||||
]);
|
||||
|
||||
cy.get('h1[aria-label*="welcome to OppLens"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "aria-label")
|
||||
.and("include", "welcome to OppLens");
|
||||
|
||||
cy.contains("Total Tenders").parent().should("contain.text", "120");
|
||||
cy.contains("Active Now").parent().should("contain.text", "72");
|
||||
cy.contains("Closing Soon").parent().should("contain.text", "12");
|
||||
cy.contains("Estimated Value").parent().should("contain.text", "€2.5M");
|
||||
|
||||
cy.contains("Tender Flow — Last 14 days").should("be.visible");
|
||||
cy.contains("By Country").should("be.visible");
|
||||
cy.contains(/^DE$/).should("be.visible");
|
||||
cy.contains("Contract Notice").should("be.visible");
|
||||
cy.contains("+70 · 70%").should("be.visible");
|
||||
|
||||
cy.contains("Railway signaling upgrade")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/tenders/closing-tender-1");
|
||||
cy.contains("Hospital equipment framework")
|
||||
.closest("a")
|
||||
.should("have.attr", "href", "/tenders/recent-tender-1");
|
||||
|
||||
cy.contains("Scraping & Translation").should("be.visible");
|
||||
cy.contains("Total Scraped Documents").parent().should("contain.text", "4,321");
|
||||
cy.contains("Total Translated Tenders").parent().should("contain.text", "321");
|
||||
cy.contains("Scraped TED per day").should("be.visible");
|
||||
cy.contains("Scraped documents per day").should("be.visible");
|
||||
cy.contains("Translated notices per day").should("be.visible");
|
||||
});
|
||||
|
||||
it("reloads scraping and translation statistics for a selected range", () => {
|
||||
visitDashboard();
|
||||
cy.wait("@statistics14Days");
|
||||
|
||||
cy.contains("button", "14d").should("have.class", "bg-primary");
|
||||
cy.contains("button", "30d").click();
|
||||
cy.wait("@statistics30Days")
|
||||
.its("request.query.days")
|
||||
.should("eq", "30");
|
||||
|
||||
cy.contains("button", "30d").should("have.class", "bg-primary");
|
||||
cy.contains("Total Scraped Documents").parent().should("contain.text", "9,876");
|
||||
cy.contains("Total Translated Tenders").parent().should("contain.text", "654");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user