6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
/**
|
||
* E2E-tester för Bounty-flödet (Skvadron G)
|
||
*
|
||
* DoD:
|
||
* - Ett komplett e2e-lyft: källbelagd → fältverifierad
|
||
* - Feedback→bounty→foto→fältverifierad sluter loopen
|
||
* - Kunddrivna kön mätbar
|
||
*/
|
||
|
||
const request = require('supertest');
|
||
const app = require('../src/app');
|
||
const Bounty = require('../src/models/bounty');
|
||
const BountyEngine = require('../src/services/bountyEngine');
|
||
|
||
describe('Skvadron G · QUIXZOOM fältflöde', () => {
|
||
|
||
describe('Bounty-motor', () => {
|
||
test('ladda bounty-data från JSON', () => {
|
||
const bountiesData = require('../../../10-data/lvx-zoomer-bounties-vag1.json');
|
||
expect(bountiesData.bounties).toHaveLength(16);
|
||
expect(bountiesData.bounties[0].bounty_id).toBe('BNTY-0001');
|
||
expect(bountiesData.bounties[0].poang).toBe(20);
|
||
});
|
||
|
||
test('ladda produktmodeller från JSON', () => {
|
||
const modelsData = require('../../../10-data/lvx-produktmodeller-auto.json');
|
||
expect(modelsData.poster).toBeDefined();
|
||
expect(modelsData.poster.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('skapa bounty via API', async () => {
|
||
const res = await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Test-bounty: Armaturens typskylt',
|
||
mal_lvx: 'LVX-TRP-0102',
|
||
foton: ['etikett läsbar', 'armaturens sida'],
|
||
ocr_falt: ['tillverkare', 'modell'],
|
||
poang: 20,
|
||
prioritet: 'hog',
|
||
position: { lat: 59.3293, lon: 18.0686, accuracy: 5 }
|
||
});
|
||
|
||
expect(res.status).toBe(201);
|
||
expect(res.body.success).toBe(true);
|
||
expect(res.body.bounty.bounty_id).toBeDefined();
|
||
expect(res.body.bounty.poang).toBe(20);
|
||
});
|
||
|
||
test('hämta bounty-status', async () => {
|
||
const createRes = await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Test-bounty',
|
||
mal_lvx: 'LVX-VAT-0101',
|
||
position: { lat: 59.3293, lon: 18.0686 }
|
||
});
|
||
|
||
const bountyId = createRes.body.bounty.bounty_id;
|
||
|
||
const res = await request(app)
|
||
.get(`/v0/bounties/${bountyId}`);
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.bounty.bounty_id).toBe(bountyId);
|
||
expect(res.body.bounty.status).toBe('oppen');
|
||
});
|
||
|
||
test('hitta bounties nära position', async () => {
|
||
// Skapa bounty på känd position
|
||
await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Närbounty',
|
||
mal_lvx: 'LVX-TRP-0101',
|
||
position: { lat: 59.3293, lon: 18.0686 }
|
||
});
|
||
|
||
const res = await request(app)
|
||
.get('/v0/bounties/nearby?lat=59.3293&lon=18.0686&radius=1');
|
||
|
||
expect(res.status).toBe(200);
|
||
expect(res.body.bounties.length).toBeGreaterThan(0);
|
||
expect(res.body.bounties[0].distance_km).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('Fältverifieringsflöde', () => {
|
||
test('komplett e2e-lyft: källbelagd → fältverifierad', async () => {
|
||
// 1. Skapa bounty
|
||
const bountyRes = await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Hydrantens gjutmärkning',
|
||
mal_lvx: 'LVX-VAT-0103',
|
||
foton: ['helbild', 'närbild gjuten text'],
|
||
ocr_falt: ['tillverkare', 'ventilstorlek', 'år', 'öppningsriktning'],
|
||
poang: 15,
|
||
position: { lat: 32.78, lon: -96.80 },
|
||
kund_ref: 'kund-123'
|
||
});
|
||
|
||
const bountyId = bountyRes.body.bounty.bounty_id;
|
||
expect(bountyRes.status).toBe(201);
|
||
|
||
// 2. Zoomer skickar foto
|
||
const submitRes = await request(app)
|
||
.post(`/v0/bounties/${bountyId}/submit`)
|
||
.send({
|
||
zoomer_id: 'zoomer-001',
|
||
position: { lat: 32.78, lon: -96.80 },
|
||
timestamp: new Date().toISOString(),
|
||
accuracy: 3,
|
||
image_base64: Buffer.from('test-image-data-hydrant-1998').toString('base64')
|
||
});
|
||
|
||
expect(submitRes.status).toBe(201);
|
||
expect(submitRes.body.submission.status).toBe('inskickad');
|
||
expect(submitRes.body.submission.poang).toBeGreaterThan(0);
|
||
const submissionId = submitRes.body.submission.id;
|
||
|
||
// 3. Godkänn submission
|
||
const approveRes = await request(app)
|
||
.post(`/v0/bounties/${bountyId}/approve`)
|
||
.send({
|
||
submission_id: submissionId,
|
||
granskningsnotering: 'Godkänd — tydligt gjutmärke'
|
||
});
|
||
|
||
expect(approveRes.status).toBe(200);
|
||
expect(approveRes.body.success).toBe(true);
|
||
expect(approveRes.body.individ).toBeDefined();
|
||
expect(approveRes.body.individ.verifieringsniva).toBe('faltverifierad');
|
||
expect(approveRes.body.message).toContain('fältverifierat');
|
||
|
||
// 4. Verifiera att notifikation skapats
|
||
const notifications = Bounty.findNotificationsByCustomer('kund-123');
|
||
expect(notifications.length).toBeGreaterThan(0);
|
||
expect(notifications[0].meddelande).toContain('fältverifierat');
|
||
});
|
||
|
||
test('dublettkontroll stoppar återinlämning', async () => {
|
||
const bountyRes = await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Dublett-test',
|
||
mal_lvx: 'LVX-TRP-0101',
|
||
position: { lat: 59.3293, lon: 18.0686 }
|
||
});
|
||
|
||
const bountyId = bountyRes.body.bounty.bounty_id;
|
||
const imageBase64 = Buffer.from('samma-bild').toString('base64');
|
||
|
||
// Första submission
|
||
const submit1 = await request(app)
|
||
.post(`/v0/bounties/${bountyId}/submit`)
|
||
.send({
|
||
zoomer_id: 'zoomer-001',
|
||
position: { lat: 59.3293, lon: 18.0686 },
|
||
timestamp: new Date().toISOString(),
|
||
image_base64: imageBase64
|
||
});
|
||
|
||
expect(submit1.status).toBe(201);
|
||
|
||
// Andra submission med samma bild
|
||
const submit2 = await request(app)
|
||
.post(`/v0/bounties/${bountyId}/submit`)
|
||
.send({
|
||
zoomer_id: 'zoomer-002',
|
||
position: { lat: 59.3293, lon: 18.0686 },
|
||
timestamp: new Date().toISOString(),
|
||
image_base64: imageBase64
|
||
});
|
||
|
||
expect(submit2.status).toBe(400);
|
||
expect(submit2.body.error).toContain('Dublett');
|
||
});
|
||
|
||
test('poängsystem: först-på-individ ×2', async () => {
|
||
const bountyRes = await request(app)
|
||
.post('/v0/bounties')
|
||
.send({
|
||
titel: 'Poäng-test',
|
||
mal_lvx: 'LVX-ELN-0102',
|
||
poang: 20,
|
||
position: { lat: 59.3293, lon: 18.0686 }
|
||
});
|
||
|
||
const bountyId = bountyRes.body.bounty.bounty_id;
|
||
|
||
// Första submission på ny position
|
||
const submitRes = await request(app)
|
||
.post(`/v0/bounties/${bountyId}/submit`)
|
||
.send({
|
||
zoomer_id: 'zoomer-001',
|
||
position: { lat: 59.3293, lon: 18.0686 },
|
||
timestamp: new Date().toISOString(),
|
||
accuracy: 2,
|
||
image_base64: Buffer.from('forsta-bild').toString('base64')
|
||
});
|
||
|
||
expect(submitRes.status).toBe(201);
|
||
expect(submitRes.body.submission.is_first_on_individual).toBe(true);
|
||
expect(submitRes.body.submission.poang_breakdown.firstOnIndividual).toBeDefined();
|
||
expect(submitRes.body.submission.poang).toBeGreaterThan(20); // ×2 = 40+
|
||
});
|
||
|
||
test('integritetsregler: auto-blurr', async () => {
|
||
const privacy = require('../src/utils/safety').checkPrivacy('ABC 123 DEF 456');
|
||
expect(privacy.safe).toBe(false);
|
||
expect(privacy.violations.length).toBeGreaterThan(0);
|
||
expect(privacy.blurred).toContain('[BLURRAT]');
|
||
});
|
||
|
||
test('säkerhetsregler: auto-blurr, inga registreringsskyltar', () => {
|
||
const { SAFETY_RULES } = require('../src/utils/safety');
|
||
expect(SAFETY_RULES.autoBlurPatterns.length).toBeGreaterThan(0);
|
||
expect(SAFETY_RULES.zoomerRules).toContain('Inga personer, ansikten eller läsbara registreringsskyltar i bild');
|
||
});
|
||
});
|
||
|
||
describe('Feedback→bounty loop', () => {
|
||
test('feedback skapar bounty automatiskt', async () => {
|
||
const res = await request(app)
|
||
.post('/v0/feedback')
|
||
.send({
|
||
kund_ref: 'kund-test-456',
|
||
position: { lat: 51.51, lon: -0.13 },
|
||
kommentar: 'Okänt grått gatuskåp utan etikett',
|
||
ocr_extraherat: []
|
||
});
|
||
|
||
expect(res.status).toBe(201);
|
||
expect(res.body.success).toBe(true);
|
||
expect(res.body.bounty).toBeDefined();
|
||
expect(res.body.bounty.bounty_id).toBeDefined();
|
||
});
|
||
|
||
test('kunddrivna kön är mätbar', async () => {
|
||
// Skapa flera feedback
|
||
for (let i = 0; i < 3; i++) {
|
||
await request(app)
|
||
.post('/v0/feedback')
|
||
.send({
|
||
kund_ref: `kund-${i}`,
|
||
position: { lat: 59.32 + i * 0.01, lon: 18.06 + i * 0.01 },
|
||
kommentar: `Feedback ${i}`
|
||
});
|
||
}
|
||
|
||
const statsRes = await request(app)
|
||
.get('/v0/stats/queue');
|
||
|
||
expect(statsRes.status).toBe(200);
|
||
expect(statsRes.body.totalt_i_flode).toBeGreaterThanOrEqual(3);
|
||
});
|
||
});
|
||
|
||
describe('LVX-I individregister', () => {
|
||
test('individ skapas vid godkännande', () => {
|
||
const individ = Bounty.createIndivid({
|
||
lvx_id: 'LVX-VAT-0103',
|
||
position_lat: 32.78,
|
||
position_lon: -96.80,
|
||
verifieringsniva: 'faltverifierad',
|
||
faltdata: { tillverkare: 'American-Darling', ar: '1998' },
|
||
bilder: ['hash123']
|
||
});
|
||
|
||
expect(individ.individ_id).toBeDefined();
|
||
expect(individ.individ_id.startsWith('LVX-I-')).toBe(true);
|
||
});
|
||
|
||
test('dublett på position uppdaterar befintlig individ', () => {
|
||
// Skapa första individ
|
||
const individ1 = Bounty.createIndivid({
|
||
lvx_id: 'LVX-TRP-0102',
|
||
position_lat: 59.3293,
|
||
position_lon: 18.0686,
|
||
faltdata: { tillverkare: 'GE' },
|
||
bilder: ['hash1']
|
||
});
|
||
|
||
// Sök nära position
|
||
const found = Bounty.findIndividByPosition(59.3293, 18.0686, 10);
|
||
expect(found).not.toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('Zoomer-profil och rykte', () => {
|
||
test('zoomer-profil skapas automatiskt', () => {
|
||
const profile = Bounty.getOrCreateZoomer('zoomer-test-001', 'Test Zoomer');
|
||
expect(profile.zoomer_id).toBe('zoomer-test-001');
|
||
expect(profile.rykte).toBe(0.5);
|
||
expect(profile.niva).toBe('ny');
|
||
});
|
||
|
||
test('rykte uppdateras vid godkännande/avvisning', () => {
|
||
Bounty.getOrCreateZoomer('zoomer-test-002');
|
||
|
||
// Godkänn
|
||
Bounty.updateZoomerStats('zoomer-test-002', 20, true);
|
||
|
||
const profile = Bounty.getOrCreateZoomer('zoomer-test-002');
|
||
expect(profile.godkanda_submissioner).toBe(1);
|
||
expect(profile.rykte).toBe(1.0);
|
||
});
|
||
});
|
||
});
|