Jobs typisiert und erweitert, nächtliche Bereinigung, Kunden-Popup bei Anruf
Job-Klassen in Frontend (Job/JobCustomer/JobCourier/JobTour) und Backend auf die neue Webhook-Form erweitert (url, finished, service, canceled, global, phone statt number – alte Schreibweise bleibt verstanden, remark je Station); Stationen legen ihre Kennung wieder als "id" ab. Adressen tragen zusätzlich role, csc_id, job_ids und url. Ein Zeitplan löscht jede Nacht um 0 Uhr Jobs, deren ordertime über vier Wochen zurückliegt. Meldet SwyxIt! einen Anruf, sucht die Startseite die Rufnummer in der Ablage (Schreibweisen-Abgleich über die Endziffern) und zeigt bei einem Kunden ein Popup mit dessen Daten; die Auftrags-URL öffnet als Knopf einen neuen Tab und schließt das Popup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,11 @@ import de.appcreation.swyxweb.config.WebhookProperties;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
// Zeitpläne wie die nächtliche Job-Bereinigung (JobCleanupService).
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties({ WebSocketProperties.class, WebhookProperties.class })
|
||||
public class BackendApplication {
|
||||
|
||||
|
||||
@@ -19,29 +19,39 @@ import org.springframework.data.mongodb.core.mapping.Document;
|
||||
* Einträge ohne Rufnummer werden deshalb übergangen – ohne sie ließe sich die
|
||||
* Dublette nicht erkennen.
|
||||
*
|
||||
* <p>Alle Quellen liefern dieselben drei Felder, nur unterschiedlich verpackt;
|
||||
* {@link #manyFrom(JsonNode)} versteht jede dieser Formen:
|
||||
* <p>{@link #manyFrom(JsonNode)} versteht jede der gelieferten Formen:
|
||||
* <ul>
|
||||
* <li>Webhook, ein Eintrag je Aufruf:
|
||||
* {@code {"name":"SYSGEN GmbH","number":"+49421409660","description":"Kunde | …"}}</li>
|
||||
* {@code {"name":"SYSGEN GmbH","number":"+491602107449","description":"Kunde | …",
|
||||
* "role":"customer","csc_id":100164,"job_ids":[21891263,21891262],"url":"https://…"}}
|
||||
* – die Felder ab {@code role} kann das Fremdsystem auch weglassen</li>
|
||||
* <li>eine JSON-Liste solcher Einträge</li>
|
||||
* <li>die Adress-Nachricht der SwyxTray-App:
|
||||
* <li>die Adress-Nachricht der SwyxTray-App (nur Name, Rufnummer, Beschreibung):
|
||||
* {@code {"addresses":[{"name":…,"number":…,"description":…}, …],"type":"addresses"}}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param id Kennung des Dokuments, vergibt die MongoDB
|
||||
* @param number Rufnummer, so wie sie gewählt wird – erkennt die Dublette
|
||||
* @param name Anzeigename, z. B. "SYSGEN GmbH" oder "Rainer Peters (HH1003)"
|
||||
* @param description Zusatz, z. B. "Kunde | SYSGEN, SYSTEME UND | NL Bremen | csc 100164"
|
||||
* oder "Kurier | NL Hamburg | PKW | cr 2024" – die Herkunft
|
||||
* eines Eintrags steht damit im Eintrag selbst
|
||||
* @param description Zusatz, z. B. "Kunde | SYSGEN, SYSTEME UND | NL Bremen"
|
||||
* oder "Kurier | NL Hamburg | PKW"
|
||||
* @param role Rolle im Fremdsystem, z. B. "customer"
|
||||
* @param cscId Kundenkennung des Fremdsystems ({@code csc_id})
|
||||
* @param jobIds Kennungen der zugehörigen Jobs ({@code job_ids}), passend
|
||||
* zur Kennung in der Sammlung {@code jobs}
|
||||
* @param url Sprungadresse des Fremdsystems zu diesem Eintrag, z. B.
|
||||
* der TAPI-Wrapper zur Rufnummer
|
||||
*/
|
||||
@Document("addresses")
|
||||
public record AddressEntry(
|
||||
@Id String id,
|
||||
String number,
|
||||
String name,
|
||||
String description) {
|
||||
String description,
|
||||
String role,
|
||||
Integer cscId,
|
||||
List<Long> jobIds,
|
||||
String url) {
|
||||
|
||||
/**
|
||||
* Liest aus beliebigem JSON alle Adressdaten-Einträge heraus (siehe die
|
||||
@@ -66,7 +76,15 @@ public record AddressEntry(
|
||||
if (!node.isObject()) return;
|
||||
String number = text(node, "number");
|
||||
if (number == null) return;
|
||||
entries.add(new AddressEntry(null, number, text(node, "name"), text(node, "description")));
|
||||
entries.add(new AddressEntry(
|
||||
null,
|
||||
number,
|
||||
text(node, "name"),
|
||||
text(node, "description"),
|
||||
text(node, "role"),
|
||||
integer(node, "csc_id"),
|
||||
longs(node, "job_ids"),
|
||||
text(node, "url")));
|
||||
}
|
||||
|
||||
/** Leere Zeichenketten schickt die SwyxTray-App als {@code ""}; hier werden sie {@code null}. */
|
||||
@@ -76,4 +94,21 @@ public record AddressEntry(
|
||||
String text = value.stringValue("");
|
||||
return text.isBlank() ? null : text;
|
||||
}
|
||||
|
||||
/** Ganzzahl der Nutzlast; alles andere wird {@code null}. */
|
||||
private static Integer integer(JsonNode node, String field) {
|
||||
JsonNode value = node.path(field);
|
||||
return value.isIntegralNumber() ? value.intValue() : null;
|
||||
}
|
||||
|
||||
/** Liste von Kennungen; fehlt sie oder ist sie keine Liste, wird sie {@code null}. */
|
||||
private static List<Long> longs(JsonNode node, String field) {
|
||||
JsonNode value = node.path(field);
|
||||
if (!value.isArray()) return null;
|
||||
List<Long> values = new ArrayList<>();
|
||||
for (JsonNode element : value) {
|
||||
if (element.isIntegralNumber()) values.add(element.longValue());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ public class ArchiveService {
|
||||
new Update()
|
||||
.set("name", entry.name())
|
||||
.set("description", entry.description())
|
||||
.set("role", entry.role())
|
||||
.set("cscId", entry.cscId())
|
||||
.set("jobIds", entry.jobIds())
|
||||
.set("url", entry.url())
|
||||
// Altbestand angleichen: Diese Felder wurden früher
|
||||
// mitgeschrieben und sollen aus der Sammlung verschwinden.
|
||||
.unset("receivedAt")
|
||||
@@ -101,6 +105,27 @@ public class ArchiveService {
|
||||
submit("Jobdaten", () -> mongo.save(job));
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht alle Jobs, deren {@code ordertime} vor der Grenze liegt (Vergleich
|
||||
* als Text, siehe {@link JobCleanupService}). Läuft anders als die
|
||||
* Schreibzugriffe direkt: Der Aufrufer ist der nächtliche Zeitplan, der auf
|
||||
* die Anzahl wartet und Fehler selbst behandelt bekommt – hier genügt die
|
||||
* Warnung im Log.
|
||||
*
|
||||
* @return wie viele Jobs gelöscht wurden; 0 auch, wenn die Datenbank nicht
|
||||
* erreichbar war
|
||||
*/
|
||||
public long removeJobsBefore(String cutoffOrdertime) {
|
||||
try {
|
||||
return mongo.remove(
|
||||
new Query(Criteria.where("ordertime").lt(cutoffOrdertime)),
|
||||
JobEntry.class).getDeletedCount();
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("MongoDB: alte Jobs nicht gelöscht ({}).", e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Legt Anruf-Ereignisse in der Sammlung {@code calls} ab. */
|
||||
public void saveCalls(List<StoredCall> calls) {
|
||||
submit("Anrufdaten", () -> calls.forEach(mongo::save));
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.appcreation.swyxweb.storage;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Räumt die Sammlung {@code jobs} auf: Jede Nacht um 0 Uhr werden die Jobs
|
||||
* gelöscht, deren {@code ordertime} mehr als vier Wochen zurückliegt.
|
||||
*
|
||||
* <p>Die {@code ordertime} liegt als ISO-8601-Zeichenkette mit Zeitzonenversatz
|
||||
* in der Datenbank (z. B. {@code 2026-07-31T16:30:00+02:00}, siehe
|
||||
* {@link JobEntry}). Verglichen wird als Text gegen eine Grenze im selben
|
||||
* Format – ISO-8601 sortiert als Text richtig; die wenigen Stunden Unschärfe
|
||||
* durch unterschiedliche Zeitzonenversätze fallen bei einer Vier-Wochen-Grenze
|
||||
* nicht ins Gewicht. Jobs ohne {@code ordertime} bleiben unangetastet – ihr
|
||||
* Alter ist nicht zu beurteilen.
|
||||
*/
|
||||
@Service
|
||||
public class JobCleanupService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JobCleanupService.class);
|
||||
|
||||
/** „Mehr als vier Wochen in der Vergangenheit" – die Grenze der Bereinigung. */
|
||||
public static final int MAX_AGE_WEEKS = 4;
|
||||
|
||||
/** Zeitzone des Zeitplans wie der Grenze: „0 Uhr" heißt 0 Uhr deutscher Zeit. */
|
||||
public static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
|
||||
|
||||
private final ArchiveService archive;
|
||||
|
||||
public JobCleanupService(ArchiveService archive) {
|
||||
this.archive = archive;
|
||||
}
|
||||
|
||||
/** Jede Nacht um 0 Uhr; verpasste Läufe (Backend aus) werden nicht nachgeholt. */
|
||||
@Scheduled(cron = "0 0 0 * * *", zone = "Europe/Berlin")
|
||||
public void purgeOldJobs() {
|
||||
String cutoff = cutoffOrdertime(ZonedDateTime.now(ZONE));
|
||||
long removed = archive.removeJobsBefore(cutoff);
|
||||
if (removed > 0) {
|
||||
log.info("Job-Bereinigung: {} Job(s) mit ordertime vor {} gelöscht.", removed, cutoff);
|
||||
} else {
|
||||
log.debug("Job-Bereinigung: nichts zu löschen (Grenze {}).", cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Grenze im Format der {@code ordertime}, z. B. {@code 2026-07-31T00:00:00+02:00}. */
|
||||
public static String cutoffOrdertime(ZonedDateTime now) {
|
||||
return now.minusWeeks(MAX_AGE_WEEKS)
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx"));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package de.appcreation.swyxweb.storage;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@@ -11,6 +12,7 @@ import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
/**
|
||||
* Ein Job (Kurierauftrag), wie ihn das Fremdsystem über den Webhook schickt
|
||||
@@ -22,18 +24,27 @@ import org.springframework.data.mongodb.core.mapping.Document;
|
||||
* {@code modified}) ersetzt also seinen alten Stand.
|
||||
*
|
||||
* <p>Die Zeitangaben bleiben die Zeichenketten der Nutzlast
|
||||
* (z. B. {@code 2026-07-31T16:30:00+02:00}): ISO-8601 sortiert auch als Text
|
||||
* richtig, und der Zeitzonenversatz des Fremdsystems geht nicht verloren.
|
||||
* (z. B. {@code 2026-08-13T13:35:27}, teils auch mit Zeitzonenversatz):
|
||||
* ISO-8601 sortiert auch als Text richtig, und die Schreibweise des
|
||||
* Fremdsystems geht nicht verloren.
|
||||
*
|
||||
* <p>Die Rufnummern hießen in einer älteren Form der Nutzlast {@code number}
|
||||
* statt {@code phone} – beide Schreibweisen werden angenommen.
|
||||
*
|
||||
* @param id Kennung des Fremdsystems, zugleich Schlüssel der Sammlung
|
||||
* @param receivedAt Eingangszeit im Backend, beim letzten Eingang dieses Jobs
|
||||
* @param url Sprungadresse des Fremdsystems zur Auftragsansicht
|
||||
* @param state Zustand des Jobs im Fremdsystem
|
||||
* @param ordertime Auftragszeit, z. B. "2026-07-31T16:30:00+02:00"
|
||||
* @param orderdate Auftragsdatum, z. B. "2026-07-31"
|
||||
* @param ordertime Auftragszeit, z. B. "2026-08-13T13:35:27"
|
||||
* @param orderdate Auftragsdatum, z. B. "2026-08-13"
|
||||
* @param modified letzte Änderung im Fremdsystem
|
||||
* @param vehicle Fahrzeugart, z. B. "Transporter"
|
||||
* @param finished wann der Job abgeschlossen wurde, sonst leer
|
||||
* @param vehicle Fahrzeugart, z. B. "Transporter XL"
|
||||
* @param service gebuchte Leistung, kann fehlen
|
||||
* @param canceled ist der Job storniert?
|
||||
* @param global bundesweite Vermittlung?
|
||||
* @param customer der beauftragende Kunde
|
||||
* @param courier der ausführende Kurier
|
||||
* @param courier der ausführende Kurier, solange keiner zugeteilt ist leer
|
||||
* @param tours die Stationen des Jobs, per {@code sort} geordnet
|
||||
*/
|
||||
@Document("jobs")
|
||||
@@ -41,11 +52,16 @@ import org.springframework.data.mongodb.core.mapping.Document;
|
||||
public record JobEntry(
|
||||
@Id long id,
|
||||
Instant receivedAt,
|
||||
String url,
|
||||
Integer state,
|
||||
String ordertime,
|
||||
String orderdate,
|
||||
String modified,
|
||||
String finished,
|
||||
String vehicle,
|
||||
String service,
|
||||
Boolean canceled,
|
||||
Boolean global,
|
||||
Customer customer,
|
||||
Courier courier,
|
||||
List<Tour> tours) {
|
||||
@@ -56,21 +72,30 @@ public record JobEntry(
|
||||
* @param cscId Kundenkennung des Fremdsystems ({@code csc_id})
|
||||
* @param name Firmenname
|
||||
* @param hq Niederlassung, z. B. "Bremen"
|
||||
* @param phone Rufnummer
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Customer(@JsonProperty("csc_id") Integer cscId, String name, String hq) {
|
||||
public record Customer(
|
||||
@JsonProperty("csc_id") Integer cscId,
|
||||
String name,
|
||||
String hq,
|
||||
@JsonAlias("number") String phone) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Der ausführende Kurier.
|
||||
*
|
||||
* @param crId Kurierkennung des Fremdsystems ({@code cr_id})
|
||||
* @param sid Kurzkennung, z. B. "B1006"
|
||||
* @param name Anzeigename
|
||||
* @param number Rufnummer
|
||||
* @param crId Kurierkennung des Fremdsystems ({@code cr_id})
|
||||
* @param sid Kurzkennung, z. B. "B1006"
|
||||
* @param name Anzeigename
|
||||
* @param phone Rufnummer
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Courier(@JsonProperty("cr_id") Integer crId, String sid, String name, String number) {
|
||||
public record Courier(
|
||||
@JsonProperty("cr_id") Integer crId,
|
||||
String sid,
|
||||
String name,
|
||||
@JsonAlias("number") String phone) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,29 +104,33 @@ public record JobEntry(
|
||||
* @param id Kennung des Fremdsystems
|
||||
* @param sort Reihenfolge innerhalb des Jobs, 1-basiert
|
||||
* @param state Zustand der Station
|
||||
* @param mode Art der Station
|
||||
* @param mode Art der Station, z. B. "pu" (Abholung) oder "del" (Zustellung)
|
||||
* @param comp Firma an der Station
|
||||
* @param person Ansprechperson
|
||||
* @param number Rufnummer an der Station, kann fehlen
|
||||
* @param phone Rufnummer an der Station, kann fehlen
|
||||
* @param street Straße und Hausnummer
|
||||
* @param zip Postleitzahl
|
||||
* @param city Ort
|
||||
* @param com Bemerkung
|
||||
* @param remark Hinweise des Fremdsystems, mehrzeilig (Referenzen, Maße …)
|
||||
* @param finished wann die Station abgeschlossen wurde, sonst leer
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Tour(
|
||||
Long id,
|
||||
// Ausdrücklich als "id" ablegen – sonst macht Spring Data auch im
|
||||
// Unterdokument ein "_id" daraus.
|
||||
@Field("id") Long id,
|
||||
Integer sort,
|
||||
Integer state,
|
||||
String mode,
|
||||
String comp,
|
||||
String person,
|
||||
String number,
|
||||
@JsonAlias("number") String phone,
|
||||
String street,
|
||||
String zip,
|
||||
String city,
|
||||
String com,
|
||||
String remark,
|
||||
String finished) {
|
||||
}
|
||||
|
||||
@@ -113,7 +142,8 @@ public record JobEntry(
|
||||
/** Füllt die Klasse aus der Webhook-Nutzlast und stempelt die Eingangszeit. */
|
||||
public static JobEntry from(JsonNode json, ObjectMapper mapper, Instant receivedAt) {
|
||||
JobEntry job = mapper.convertValue(json, JobEntry.class);
|
||||
return new JobEntry(job.id(), receivedAt, job.state(), job.ordertime(), job.orderdate(),
|
||||
job.modified(), job.vehicle(), job.customer(), job.courier(), job.tours());
|
||||
return new JobEntry(job.id(), receivedAt, job.url(), job.state(), job.ordertime(),
|
||||
job.orderdate(), job.modified(), job.finished(), job.vehicle(), job.service(),
|
||||
job.canceled(), job.global(), job.customer(), job.courier(), job.tours());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class AddressControllerTests {
|
||||
@Test
|
||||
void listPassesQueryToArchive() throws Exception {
|
||||
when(archive.addresses("muster"))
|
||||
.thenReturn(List.of(new AddressEntry("1", "+493012345", "Muster GmbH", null)));
|
||||
.thenReturn(List.of(new AddressEntry("1", "+493012345", "Muster GmbH", null, null, null, null, null)));
|
||||
|
||||
mvc.perform(get("/api/addresses").param("q", "muster"))
|
||||
.andExpect(status().isOk())
|
||||
|
||||
@@ -27,14 +27,34 @@ class AddressEntryTests {
|
||||
@Test
|
||||
void readsSingleWebhookEntry() {
|
||||
List<AddressEntry> entries = parse("""
|
||||
{"name":"SYSGEN GmbH","number":"+49421409660",
|
||||
"description":"Kunde | SYSGEN, SYSTEME UND | NL Bremen | csc 100164"}""");
|
||||
{"name":"SYSGEN GmbH","number":"+491602107449",
|
||||
"description":"Kunde | SYSGEN, SYSTEME UND | NL Bremen",
|
||||
"role":"customer","csc_id":100164,"job_ids":[21891263,21891262],
|
||||
"url":"https://test.sb.assecutor.de/admin/tapi_wrapper.php?phoneNo=01602107449"}""");
|
||||
|
||||
assertThat(entries).hasSize(1);
|
||||
AddressEntry entry = entries.getFirst();
|
||||
assertThat(entry.name()).isEqualTo("SYSGEN GmbH");
|
||||
assertThat(entry.number()).isEqualTo("+49421409660");
|
||||
assertThat(entry.description()).isEqualTo("Kunde | SYSGEN, SYSTEME UND | NL Bremen | csc 100164");
|
||||
assertThat(entry.number()).isEqualTo("+491602107449");
|
||||
assertThat(entry.description()).isEqualTo("Kunde | SYSGEN, SYSTEME UND | NL Bremen");
|
||||
assertThat(entry.role()).isEqualTo("customer");
|
||||
assertThat(entry.cscId()).isEqualTo(100164);
|
||||
assertThat(entry.jobIds()).containsExactly(21891263L, 21891262L);
|
||||
assertThat(entry.url())
|
||||
.isEqualTo("https://test.sb.assecutor.de/admin/tapi_wrapper.php?phoneNo=01602107449");
|
||||
}
|
||||
|
||||
/** Die Felder ab {@code role} darf das Fremdsystem auch weglassen. */
|
||||
@Test
|
||||
void extraFieldsStayNullWhenAbsent() {
|
||||
List<AddressEntry> entries = parse("""
|
||||
{"name":"SYSGEN GmbH","number":"+49421409660","description":"Kunde | …"}""");
|
||||
|
||||
AddressEntry entry = entries.getFirst();
|
||||
assertThat(entry.role()).isNull();
|
||||
assertThat(entry.cscId()).isNull();
|
||||
assertThat(entry.jobIds()).isNull();
|
||||
assertThat(entry.url()).isNull();
|
||||
}
|
||||
|
||||
/** Kuriere und Jobs kommen analog – auch als Liste. */
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package de.appcreation.swyxweb;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.appcreation.swyxweb.storage.JobCleanupService;
|
||||
|
||||
/**
|
||||
* Die nächtliche Job-Bereinigung vergleicht die {@code ordertime} als Text –
|
||||
* hier wird die Grenze und der Vergleich gegen das Format der Nutzlast geprüft.
|
||||
*/
|
||||
class JobCleanupServiceTests {
|
||||
|
||||
@Test
|
||||
void cutoffLiesFourWeeksBackInOrdertimeFormat() {
|
||||
ZonedDateTime now = ZonedDateTime.of(2026, 8, 28, 0, 0, 0, 0, JobCleanupService.ZONE);
|
||||
|
||||
// Vier Wochen vor dem 28.08. ist der 31.07.; im Sommer gilt +02:00.
|
||||
assertThat(JobCleanupService.cutoffOrdertime(now)).isEqualTo("2026-07-31T00:00:00+02:00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordertimeStringsCompareChronologically() {
|
||||
String cutoff = "2026-07-31T00:00:00+02:00";
|
||||
|
||||
assertThat("2026-07-30T18:14:02+02:00").isLessThan(cutoff);
|
||||
assertThat("2026-08-01T08:00:00+02:00").isGreaterThan(cutoff);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import de.appcreation.swyxweb.storage.JobEntry;
|
||||
|
||||
/**
|
||||
* {@link JobEntry} wird aus der Webhook-Nutzlast mit {@code "type":"job"}
|
||||
* gefüllt – samt Kunde, Kurier und Stationen.
|
||||
* gefüllt – samt Kunde, Kurier und Stationen. Eine ältere Form der Nutzlast
|
||||
* nannte die Rufnummern {@code number} statt {@code phone}; auch sie wird
|
||||
* weiterhin verstanden.
|
||||
*/
|
||||
class JobEntryTests {
|
||||
|
||||
@@ -22,21 +24,30 @@ class JobEntryTests {
|
||||
private static final String SAMPLE = """
|
||||
{
|
||||
"type": "job",
|
||||
"id": 21891253,
|
||||
"state": 2,
|
||||
"ordertime": "2026-07-31T16:30:00+02:00",
|
||||
"orderdate": "2026-07-31",
|
||||
"modified": "2026-07-30T18:14:02+02:00",
|
||||
"vehicle": "Transporter",
|
||||
"customer": { "csc_id": 868374, "name": "B+M isol nord GmbH", "hq": "Bremen" },
|
||||
"courier": { "cr_id": 14116, "sid": "B1006", "name": "CA Kurier B1006", "number": "+4917xxxxxxx" },
|
||||
"id": 21891263,
|
||||
"url": "https://test.sb.assecutor.de/admin/jb_detail.php?job_id=21891263",
|
||||
"state": 9,
|
||||
"ordertime": "2026-08-13T13:35:27",
|
||||
"orderdate": "2026-08-13",
|
||||
"modified": "2026-08-13T13:35:27",
|
||||
"finished": null,
|
||||
"vehicle": "Transporter XL",
|
||||
"service": null,
|
||||
"canceled": false,
|
||||
"global": false,
|
||||
"customer": { "csc_id": 100164, "name": "SYSGEN GmbH", "hq": "Bremen",
|
||||
"phone": "+491602107449" },
|
||||
"courier": null,
|
||||
"tours": [
|
||||
{ "id": 22396738, "sort": 1, "state": 1, "mode": "", "comp": "B+M isol nord GmbH",
|
||||
"person": "", "number": "+4942037010xx", "street": "Löwenhof 2", "zip": "28844",
|
||||
"city": "Weyhe-Dreye", "com": "", "finished": "2026-07-30T18:13:11+02:00" },
|
||||
{ "id": 22396739, "sort": 2, "state": 1, "mode": "", "comp": "Reichspräsident Ebert Kaserne",
|
||||
"person": "", "number": null, "street": "Osdorfer Landstr. 365", "zip": "22589",
|
||||
"city": "Hamburg", "com": "", "finished": "2026-07-30T18:13:51+02:00" }
|
||||
{ "id": 22396758, "sort": 1, "state": 0, "mode": "del", "comp": "SYSGEN GmbH",
|
||||
"person": "Frau Hoffmann", "phone": "+49421409660", "com": null,
|
||||
"remark": "~~~\\nHebebühne benötigt.\\n~~~", "street": "Am Hallacker 48",
|
||||
"zip": "28327", "city": "Bremen", "finished": null },
|
||||
{ "id": 22396759, "sort": 2, "state": 0, "mode": "pu",
|
||||
"comp": "Super Micro Computer B.V.", "person": null, "phone": null, "com": null,
|
||||
"remark": "Abholreferenz: 8801420234\\nAnzahl an Paletten: 8",
|
||||
"street": "Het Sterrenbeeld 12-16", "zip": "5215", "city": "'s-Hertogenbosch",
|
||||
"finished": null }
|
||||
]
|
||||
}""";
|
||||
|
||||
@@ -49,33 +60,60 @@ class JobEntryTests {
|
||||
|
||||
@Test
|
||||
void fillsAllFieldsFromSample() {
|
||||
Instant receivedAt = Instant.parse("2026-08-26T10:00:00Z");
|
||||
Instant receivedAt = Instant.parse("2026-08-28T10:00:00Z");
|
||||
JsonNode json = mapper.readTree(SAMPLE);
|
||||
|
||||
JobEntry job = JobEntry.from(json, mapper, receivedAt);
|
||||
|
||||
assertThat(job.id()).isEqualTo(21891253L);
|
||||
assertThat(job.id()).isEqualTo(21891263L);
|
||||
assertThat(job.receivedAt()).isEqualTo(receivedAt);
|
||||
assertThat(job.state()).isEqualTo(2);
|
||||
assertThat(job.ordertime()).isEqualTo("2026-07-31T16:30:00+02:00");
|
||||
assertThat(job.orderdate()).isEqualTo("2026-07-31");
|
||||
assertThat(job.vehicle()).isEqualTo("Transporter");
|
||||
assertThat(job.url()).isEqualTo("https://test.sb.assecutor.de/admin/jb_detail.php?job_id=21891263");
|
||||
assertThat(job.state()).isEqualTo(9);
|
||||
assertThat(job.ordertime()).isEqualTo("2026-08-13T13:35:27");
|
||||
assertThat(job.orderdate()).isEqualTo("2026-08-13");
|
||||
assertThat(job.finished()).isNull();
|
||||
assertThat(job.vehicle()).isEqualTo("Transporter XL");
|
||||
assertThat(job.service()).isNull();
|
||||
assertThat(job.canceled()).isFalse();
|
||||
assertThat(job.global()).isFalse();
|
||||
|
||||
assertThat(job.customer().cscId()).isEqualTo(868374);
|
||||
assertThat(job.customer().name()).isEqualTo("B+M isol nord GmbH");
|
||||
assertThat(job.customer().cscId()).isEqualTo(100164);
|
||||
assertThat(job.customer().name()).isEqualTo("SYSGEN GmbH");
|
||||
assertThat(job.customer().hq()).isEqualTo("Bremen");
|
||||
assertThat(job.customer().phone()).isEqualTo("+491602107449");
|
||||
|
||||
assertThat(job.courier().crId()).isEqualTo(14116);
|
||||
assertThat(job.courier().sid()).isEqualTo("B1006");
|
||||
assertThat(job.courier().number()).isEqualTo("+4917xxxxxxx");
|
||||
// Noch kein Kurier zugeteilt.
|
||||
assertThat(job.courier()).isNull();
|
||||
|
||||
assertThat(job.tours()).hasSize(2);
|
||||
JobEntry.Tour first = job.tours().getFirst();
|
||||
assertThat(first.id()).isEqualTo(22396738L);
|
||||
assertThat(first.id()).isEqualTo(22396758L);
|
||||
assertThat(first.sort()).isEqualTo(1);
|
||||
assertThat(first.street()).isEqualTo("Löwenhof 2");
|
||||
assertThat(first.finished()).isEqualTo("2026-07-30T18:13:11+02:00");
|
||||
assertThat(first.mode()).isEqualTo("del");
|
||||
assertThat(first.person()).isEqualTo("Frau Hoffmann");
|
||||
assertThat(first.phone()).isEqualTo("+49421409660");
|
||||
assertThat(first.remark()).contains("Hebebühne benötigt.");
|
||||
assertThat(first.street()).isEqualTo("Am Hallacker 48");
|
||||
// Fehlende Rufnummer der zweiten Station bleibt null.
|
||||
assertThat(job.tours().get(1).number()).isNull();
|
||||
assertThat(job.tours().get(1).phone()).isNull();
|
||||
assertThat(job.tours().get(1).city()).isEqualTo("'s-Hertogenbosch");
|
||||
}
|
||||
|
||||
/** Die ältere Form nannte die Rufnummern {@code number} statt {@code phone}. */
|
||||
@Test
|
||||
void acceptsLegacyNumberFields() {
|
||||
JsonNode json = mapper.readTree("""
|
||||
{
|
||||
"type": "job",
|
||||
"id": 21891253,
|
||||
"courier": { "cr_id": 14116, "sid": "B1006", "name": "CA Kurier B1006",
|
||||
"number": "+4917xxxxxxx" },
|
||||
"tours": [ { "id": 22396738, "sort": 1, "number": "+4942037010xx" } ]
|
||||
}""");
|
||||
|
||||
JobEntry job = JobEntry.from(json, mapper, Instant.parse("2026-08-28T10:00:00Z"));
|
||||
|
||||
assertThat(job.courier().phone()).isEqualTo("+4917xxxxxxx");
|
||||
assertThat(job.tours().getFirst().phone()).isEqualTo("+4942037010xx");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user