job,long customer,boolean recovered){
+ String language=c.parameter(0,"JOBDETAILS_EMAIL_LANGUAGE_"+customer),global=c.parameter(0,"SYSTEM_LANGUAGE_DEFAULT");if(language.isBlank())language="0";if(global.isBlank())global="0";
+ c.language=OrderMailLanguage.load(c.db,language,false,language.equals(global)?"include/mcglobal.inc.php":"include/automailer"+(recovered?8:6)+".php");
+ String at=dateTime(job,"jb_ordertime").format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"));
+ String subject=recovered?c.t("Ihr Auftrag")+" "+id(job,"jb_id")+" "+c.t("wurde erfolgreich vermittelt"):c.t("Verzögerung bei Ihrem Auftrag Nr.: ")+" "+id(job,"jb_id");
+ var body=new StringBuilder("").append(rich(c.mailText("MAIL_PRE_SALUTATION_TEXT","Sehr geehrte Damen und Herren,",false))).append("
");
+ if(recovered){body.append(escape(c.t("wir möchten Sie darüber informieren, dass Ihr aktueller Auftrag, geplant für"))).append(' ').append(escape(at)).append(", ").append(escape(c.t("nun erfolgreich an einen Boten vermittelt wurde."))).append("
")
+ .append(escape(c.t("Unser Fahrer befindet sich bereits auf dem Weg zu Ihnen und wird die Lieferung schnellstmöglich zustellen."))).append("
").append(escape(c.t("Sollten Sie weitere Fragen haben, stehen wir Ihnen gerne zur Verfügung."))).append("
").append(escape(c.t("Wir danken Ihnen für Ihr Vertrauen in STADTBOTE und wünschen Ihnen einen angenehmen Tag.")));}
+ else{body.append(escape(c.t("wir möchten Sie darüber informieren, dass es bei Ihrem aktuellen Auftrag geplant für"))).append(' ').append(escape(at)).append(' ').append(escape(c.t("zu einer Verzögerung in der Vermittlung kommt."))).append("
")
+ .append(escape(c.t("Unser Team arbeitet aktiv daran, einen verfügbaren Boten für Ihren Auftrag zu finden. Wir prüfen alle Optionen, um Ihre Lieferung schnellstmöglich zu vermitteln."))).append("
").append(escape(c.t("Sobald Ihr Auftrag erfolgreich vermittelt wurde, werden wir Sie per Mail informieren."))).append("
").append(escape(c.t("Wir hoffen auf Ihr Verständnis und bedanken uns für Ihr Vertrauen in STADTBOTE.")));}
+ body.append("
").append(rich(c.mailText("MAIL_TEXT_REGARDS","Mit freundlichem Gruß,",false))).append("
").append(rich(c.t(c.local("MAIL_SALUTATION_TEXT")))).append("
");
+ var warnings=new ArrayList();String logo=c.local("IMG_LOGO_NAME"),height=c.local("IMG_LOGO_HEIGHT"),width=c.local("IMG_LOGO_WIDTH");
+ if(!logo.isBlank()&&!height.isBlank()&&!width.isBlank()&&!c.legacy.isBlank()){
+ if(!logo.matches("[A-Za-z0-9_. -]{1,150}")||Set.of(".","..").contains(logo)||!height.matches("[1-9][0-9]{0,3}")||!width.matches("[1-9][0-9]{0,3}"))throw bad("Die Logo-Vorgaben der Vermittlungsmail sind ungültig.");
+ String web=c.local("MAIL_FOOTER_WEB_PAGE"),link=web.isBlank()?"":Objects.toString(ApiController.safeLegacyUrl(web.contains("://")?web:"http://"+web),"");if(!web.isBlank()&&link.isBlank())throw bad("Die Homepage der Mailfußzeile ist ungültig.");
+ body.append("");if(!link.isBlank())body.append("");body.append("
");if(!link.isBlank())body.append("");body.append("
");warnings.add("Das Logo wird beim Empfänger von der bisherigen Anwendungsadresse geladen.");
+ }
+ if(c.local("MAIL_FOOTER_ENABLED").equals("1")){
+ for(String key:List.of("MAIL_FOOTER_ADDRESS","MAIL_FOOTER_CONTACT","MAIL_FOOTER_IMPRESSUM"))body.append("").append(rich(c.local(key))).append("
");
+ for(int i=1;i<=5;i++){String value=c.fallback("MAIL_FOOTER_RESPONSIBILITY"+(i==1?"":"_"+i));if(value.isBlank())break;body.append("").append(rich(value)).append("
");}body.append("").append(rich(c.local("MAIL_FOOTER_THINK_GREEN"))).append("
");
+ }
+ String html=""+body+"";
+ if(html.length()>400000)throw bad("Die Vermittlungsmail ist zu umfangreich.");warnings.addAll(c.language.warnings());return new Content(subject,html,List.copyOf(warnings),revision(List.of(job,customer,recovered,c.parameters,html,subject,c.language.revision)),false);
+ }
+}
diff --git a/backend/src/main/java/de/stadtbote/votian/AutomaticOfferPolicy.java b/backend/src/main/java/de/stadtbote/votian/AutomaticOfferPolicy.java
new file mode 100644
index 0000000..0c1aa2d
--- /dev/null
+++ b/backend/src/main/java/de/stadtbote/votian/AutomaticOfferPolicy.java
@@ -0,0 +1,59 @@
+package de.stadtbote.votian;
+
+import java.time.*;
+import java.util.*;
+import static de.stadtbote.votian.LegacyMapping.*;
+import static de.stadtbote.votian.AdminSupport.*;
+
+/** Rules of the current PHP autoranking_single.php, independent of scheduling and persistence. */
+final class AutomaticOfferPolicy {
+ record Settings(Map local,Map global){
+ String local(String key){return local.getOrDefault(key,"").trim();}
+ String inherited(String key){String v=local(key);return v.isEmpty()?global.getOrDefault(key,"").trim():v;}
+ int number(String key,int fallback,int max){String v=local(key);if(v.isEmpty())return fallback;if(!v.matches("[0-9]{1,6}"))throw bad(key+" ist ungültig.");int n=Integer.parseInt(v);if(n>max)throw bad(key+" überschreitet "+max+".");return n;}
+ }
+ record Candidate(long courier,long vehicle,String sid,String name,String postal,String availableAt,int group,String groupName,int challenges,List blockers){boolean eligible(){return blockers.isEmpty();}}
+ record Plan(String state,String message,LocalDateTime startsAt,int iterations,boolean reserved,boolean onlyFavoured,boolean ignoreFavoured,List candidates){
+ List rounds(){if(reserved)return List.of("RESERVED");var r=new ArrayList();if(!ignoreFavoured)for(int i=0;iround.equals("RESERVED")?c.group()==0:round.equals("FAVOURITES")?c.group()==1:c.group()>=2).findFirst().orElse(null);}
+ }
+ static String commonBlock(Map j,Settings s,LocalDateTime now){
+ if(id(j,"jb_status")!=8)return "Der Auftrag steht nicht im Autoranking.";
+ String guard=OrderRankingBatch.blocked(j,now);if(!guard.isEmpty())return guard;
+ if(id(j,"jb_longhaul")==1)return "Fernaufträge verwenden die BWV-Vermittlung.";
+ if(id(j,"jb_globaljob")==1)return "Der Auftrag wird bereits allen Kurieren angeboten.";
+ if(id(j,"hq_id_dispo")<=0)return "Die Dispositionsniederlassung fehlt.";
+ if(!s.local("AUTORANKING_ASSIGNMENT_ENABLED").equals("1"))return "Automatische Angebote sind für die Dispositionsniederlassung ausgeschaltet.";
+ return "";
+ }
+ static List blockers(Map j,Map v,Settings s,int group,int challenges,int historyCount,boolean occupied,boolean quarantine,Set countries,List