OpenSource Projekt-Management-Software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

591 lines
26 KiB

/* © SRSoftware 2025 */
package de.srsoftware.umbrella.documents;
import static de.srsoftware.document.mustang.Constants.*;
import static de.srsoftware.document.mustang.Constants.KEY_DATA;
import static de.srsoftware.document.mustang.Constants.KEY_PDF;
import static de.srsoftware.document.mustang.Constants.KEY_PRODUCER;
import static de.srsoftware.document.mustang.Constants.KEY_TEMPLATE;
import static de.srsoftware.document.zugferd.data.UnitCode.*;
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.MimeType.MIME_PDF;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.tools.Strings.escapeHtmlEntities;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Constants.FIELD_AMOUNT;
import static de.srsoftware.umbrella.core.Constants.FIELD_COMPANY;
import static de.srsoftware.umbrella.core.Constants.FIELD_CUSTOMER;
import static de.srsoftware.umbrella.core.Constants.FIELD_DOCUMENT;
import static de.srsoftware.umbrella.core.Constants.FIELD_ITEM_CODE;
import static de.srsoftware.umbrella.core.Constants.FIELD_PRICE_FORMAT;
import static de.srsoftware.umbrella.core.Constants.FIELD_TIME_ID;
import static de.srsoftware.umbrella.core.Constants.FIELD_TYPE;
import static de.srsoftware.umbrella.core.Constants.FIELD_UNIT;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.Paths.*;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.Util.request;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Document.State.NEW;
import static de.srsoftware.umbrella.core.model.Document.State.SENT;
import static de.srsoftware.umbrella.documents.Constants.*;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.WARNING;
import static java.net.HttpURLConnection.*;
import static java.util.stream.Collectors.toMap;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.document.api.Content;
import de.srsoftware.document.api.DocumentRegistry;
import de.srsoftware.document.api.RenderError;
import de.srsoftware.document.files.DocumentDirectory;
import de.srsoftware.document.processor.latex.LatexFactory;
import de.srsoftware.document.processor.weasyprint.WeasyFactory;
import de.srsoftware.document.zugferd.ZugferdFactory;
import de.srsoftware.document.zugferd.data.*;
import de.srsoftware.document.zugferd.data.Currency;
import de.srsoftware.tools.Pair;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.Paths;
import de.srsoftware.umbrella.core.api.*;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Customer;
import de.srsoftware.umbrella.documents.model.*;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.json.JSONArray;
import org.json.JSONObject;
public class DocumentApi extends BaseHandler implements DocumentService {
private static final Predicate<de.srsoftware.document.api.Document> ZUGFERD_FILTER = document -> document.id().equals("Zugferd");
private final DocumentRegistry registry = new DocumentRegistry();
private final Configuration config;
private final DocumentDb db;
public DocumentApi(Configuration config) throws UmbrellaException {
super();
this.config = config;
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
db = new SqliteDb(connect(dbFile));
ModuleRegistry.add(this);
Optional<String> templates = config.get(CONFIG_TEMPLATES);
if (templates.isEmpty()) throw missingFieldException(CONFIG_TEMPLATES);
this.registry.add(new DocumentDirectory(new File(templates.get())));
this.registry.add(new TemplateProcessor(), new LatexFactory(), new WeasyFactory(), new ZugferdFactory());
this.registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d));
}
@Override
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
long docId = Long.parseLong(head);
return switch (path.pop()){
case POSITION -> deletePosition(ex,docId,user.get());
case null -> deleteDocument(ex,docId,user.get());
default -> super.doDelete(path,ex);
};
} catch (NumberFormatException ignored) {
return super.doDelete(path,ex);
} catch (UmbrellaException e) {
return send(ex,e);
}
}
private boolean deleteDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var doc = db.loadDoc(docId);
var companyId = doc.companyId();
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",doc.companyId());
if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent");
return sendContent(ex,db.deleteDoc(docId));
}
private boolean deletePosition(HttpExchange ex, long docId, UmbrellaUser user) throws UmbrellaException, IOException {
var doc = db.loadDoc(docId);
var companyId = doc.companyId();
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",doc.companyId());
if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent");
var json = json(ex);
if (!(json.has(POSITION) && json.get(POSITION) instanceof Number number)) throw missingFieldException(POSITION);
db.dropPosition(docId,number.longValue());
return send(ex,db.loadDoc(docId).positions());
}
@Override
public boolean doGet(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head){
case CONTACTS -> getContacts(ex,user.get(),token.orElse(null));
case PATH_TYPES -> getDocTypes(ex);
case STATES -> getDocStates(ex);
case null -> super.doGet(path,ex);
default -> {
var docId = Long.parseLong(head);
head = path.pop();
yield switch (head){
case null -> getDocument(ex,docId,user.get());
case PATH_PDF -> getRenderedDocument(ex,docId,user.get());
case Paths.SETTINGS -> getDocumentSettings(ex,docId,user.get());
default -> super.doGet(path,ex);
};
}
};
} catch (NumberFormatException ignored) {
return super.doGet(path,ex);
} catch (UmbrellaException e) {
return send(ex,e);
}
}
@Override
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
var docId = Long.parseLong(head);
head = path.pop();
return switch (head){
case POSITION -> patchDocumentPosition(docId,user.get(),ex);
case null -> patchDocument(docId,user.get(),ex);
default -> super.doPatch(path,ex);
};
} catch (NumberFormatException n){
return sendContent(ex,HTTP_UNPROCESSABLE,"Invalid document id");
} catch (UmbrellaException e) {
return send(ex,e);
}
}
@Override
public boolean doPost(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head){
case LIST -> listCompaniesDocuments(ex,user.get());
case SEARCH -> postSearch(ex,user.get());
case TEMPLATES -> postTemplateList(ex,user.get());
case null -> postDocument(ex,user.get());
default -> postToDocument(ex,path,user.get(),Long.parseLong(head));
};
} catch (NumberFormatException ignored) {
return super.doPost(path,ex);
} catch (UmbrellaException e) {
return send(ex,e);
}
}
private boolean getContacts(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException {
return sendContent(ex,getLegacyContacts(ex,user,token));
}
private boolean getDocStates(HttpExchange ex) throws IOException {
var map = Stream.of(Document.State.values()).collect(toMap(Document.State::code, Document.State::name));
return sendContent(ex,map);
}
private boolean getDocTypes(HttpExchange ex) throws UmbrellaException, IOException {
var types = db.listTypes();
var map = types.values().stream().collect(toMap(Type::id, Type::name));
return sendContent(ex,map);
}
private Tuple<Document,Company> getDocument(long docId, UmbrellaUser user) throws UmbrellaException {
var doc = db.loadDoc(docId);
var companyId = doc.companyId();
var company = companyService().get(companyId);
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name());
return Tuple.of(doc,company);
}
private Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException {
var tuple = getDocument(docId,user);
var company = tuple.b;
var sep = company.decimalSeparator();
var doc = tuple.a;
if (sep != null) doc.setDecimalSeparator(sep);
doc.setCompanyName(company.name());
return doc;
}
private boolean getDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var doc = getDocumentWithCompanyData(docId,user);
return sendContent(ex,doc.renderToMap());
}
private boolean getDocumentSettings(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var tuple = getDocument(docId,user);
var doc = tuple.a;
var company = tuple.b;
var settings = db.getCustomerSettings(company.id(),doc.type(),doc.customer().id());
return sendContent(ex,settings);
}
private DocumentData convert(Document document) throws UmbrellaException {
var currency = switch (document.currency()){
case "€" -> Currency.EUR;
case "$" -> Currency.USD;
default -> throw unprocessable("Unsupported currency: ",document.currency());
};
var typeCode = switch (document.type().name()){
case "invoice" -> TypeCode.HANDELSRECHNUNG;
default -> throw unprocessable("Unsupported document type: ",document.type().name());
};
var countryID = CountryCode.DE;
LOG.log(WARNING,"countryID is hardcoded to be \"DE\", should be field of company!");
var sender = document.sender();
var match = POST_CODE.matcher(sender.name());
if (!match.find()) throw unprocessable(ERROR_ADDRESS_MISSING, SENDER);
var name = match.group(1).trim();
var streetAddress = match.group(2).trim();
var postCode = match.group(3);
var city = match.group(4).trim();
var taxNumber = sender.taxNumber();
if (!taxNumber.startsWith("DE")) throw unprocessable("Invalid sender tax number ({0})!",taxNumber);
var author = new Author(name,countryID,postCode,city,streetAddress,taxNumber);
match = POST_CODE.matcher(document.customer().name());
if (!match.find()) throw unprocessable(ERROR_ADDRESS_MISSING,FIELD_CUSTOMER);
name = escapeHtmlEntities(match.group(1).trim());
streetAddress = escapeHtmlEntities(match.group(2).trim());
postCode = match.group(3);
city = match.group(4).trim();
var customer = new de.srsoftware.document.zugferd.data.Customer(name,countryID,postCode,streetAddress,city);
var footer = document.footer();
var head = document.head();
var notes = new ArrayList<String>();
if (!head.isBlank()) notes.add(head);
if (!footer.isBlank()) notes.add(footer);
LocalDate deliveryDate;
try {
deliveryDate = LocalDate.parse(document.delivery());
} catch (RuntimeException ex) {
throw unprocessable("\"{0}\" is not a valid delivery date (cannot be parsed)!",document.delivery());
}
var lineItems = new ArrayList<LineItem>();
for (var entry : document.positions().entrySet()){
var number = entry.getKey();
var pos = entry.getValue();
UnitCode unit = switch (pos.unit()){
case "d" -> per_day;
case "h", "Stunden" -> hours;
case "jährlich" -> per_year;
case "pauschal" -> fixed;
case "Stück" -> pieces;
default -> throw unprocessable("No unit code defined for {0}",pos.unit());
};
var percent = pos.tax();
var taxType = percent == 0 ? TaxType.Z : TaxType.S;
LOG.log(WARNING,"tax type is hardcoded to be \"{0}\", should be field of document / document position!",taxType.name());
var taxSet = new LineItemTaxSet(percent*100L,taxType);
// bei Gutschriften soll die Menge negativ sein, nicht aber der Einheitspreis. Deshalb wird das ggf. invertiert.
var neg = pos.unitPrice() < 0;
var unitPrice = (neg ? -1 : 1) * pos.unitPrice();
var amount = (neg ? -1 : 1) * pos.amount();
lineItems.add(new LineItem(number,pos.itemCode(),pos.title(),pos.description(),null,unitPrice,unit,amount,taxSet));
}
String terms = footer;
return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems);
}
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
var template = document.template().name();
var templateName = template+".html.pdf";
var type = document.type().name();
var zugferd = "invoice".equals(type);
Predicate<de.srsoftware.document.api.Document> filter = zugferd ? ZUGFERD_FILTER : doc -> doc.name().equals(templateName);
var optDoc = registry.documents()
.filter(filter)
.findAny();
if (optDoc.isEmpty()) throw UmbrellaException.notFound("Cannot render {0} {1}: Missing template \"{2}\"",type,document.number(),template);
Function<String,String> translate = text -> translator().translate(user.language(),text);
var pdfData = new HashMap<String,Object>();
pdfData.put(FIELD_DOCUMENT,document.renderToMap());
pdfData.put("translate",translate);
pdfData.put(USER,user);
pdfData.put(FIELD_PRICE_FORMAT, priceFormat(document.currency(),user.language()));
Map<String,Object> data = pdfData;
if (zugferd) {// create additional data
data = Map.of(
KEY_XML,Map.of(
KEY_PROFILE,"EN16931",
KEY_DATA,convert(document)
),
KEY_PDF,Map.of(
KEY_TEMPLATE, templateName,
KEY_DATA, pdfData
),
KEY_PRODUCER,"SRSoftware Umbrella"
);
}
var source = optDoc.get();
var rendered = source.render(data);
if (rendered instanceof RenderError err) throw new UmbrellaException(500,"Failed to render {0}: {1}",source.name(),err.toString());
if (!(rendered instanceof Content content)) throw new UmbrellaException(500,"Unknown result type ({0}) returned from render process!",rendered.getClass().getSimpleName());
return content;
}
private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var document = getDocumentWithCompanyData(docId,user);
var content = renderDocument(document,user);
var headers = ex.getResponseHeaders();
headers.add(CONTENT_TYPE, MIME_PDF);
headers.add(CONTENT_DISPOSITION,"attachment; filename=\""+document.number()+".pdf\"");
return sendContent(ex,content.bytes());
}
private JSONArray getLegacyContacts(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException {
var location = config.get("umbrella.modules.contact.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.contact.baseUrl not configured!"));
var resp = request(location, token.asMap(),MIME_FORM_URL,null);
if (!(resp instanceof String s && s.startsWith("["))) throw new UmbrellaException(500,"{0} did not return JSON Array!",location);
return new JSONArray(s);
}
public Map<Long, Document> list(long companyId) throws UmbrellaException{
return db.listDocs(companyId);
}
private boolean listCompaniesDocuments(HttpExchange ex, UmbrellaUser user) throws UmbrellaException {
try {
var json = json(ex);
if (!json.has(COMPANY)) throw missingFieldException(COMPANY);
long companyId = json.getLong(COMPANY);
var company = companyService().get(companyId);
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company);
var docs = list(companyId);
var map = new HashMap<Long,Object>();
for (var entry : docs.entrySet()) map.put(entry.getKey(),entry.getValue().summary());
return sendContent(ex,new JSONObject(map).toString(2));
} catch (IOException e) {
LOG.log(WARNING,"Failed to parse JSON data from request",e);
throw new UmbrellaException( 500,"Failed to parse JSON data from request").causedBy(e);
}
}
@Override
public Map<Long, Map<Long, String>> docsReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
return db.docReferencedByTimes(timeIds);
}
private boolean patchDocument(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException {
var doc = getDocument(docId,user).a;
var data = json(ex);
doc.patch(data);
if (doc.isDirty(FIELD_FOOTER,FIELD_HEAD)) {
var settings = db.getCustomerSettings(doc.companyId(),doc.type(),doc.customer().id());
if (settings == null) settings = CustomerSettings.empty();
db.save(doc.companyId(),doc.type(),doc.customer().id(), settings.patch(data));
}
db.save(doc);
return ok(ex);
}
private boolean send(HttpExchange ex,PositionList positions) throws IOException {
return sendContent(ex,positions.entrySet().stream().collect(toMap(Map.Entry::getKey,entry -> entry.getValue().renderToMap())));
}
private boolean patchDocumentPosition(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException {
var doc = getDocument(docId,user).a;
if (doc.state() != NEW) throw forbidden("Document has already been send and is write-protected!");
var json = json(ex);
var step = json.has(MOVE) && json.get(MOVE) instanceof Number num ? num.intValue() : 0;
Integer number = json.has(POSITION) && json.get(POSITION) instanceof Number num ? num.intValue() : null;
if (isSet(number) && step != 0) {
var pos2 = number+step;
if (number >0 && pos2>0 && number <=doc.positions().size() && pos2<=doc.positions().size()){
db.switchPositions(docId,new Pair<>(number,pos2));
doc = db.loadDoc(docId);
}
}
return send(ex,db.save(doc).positions());
}
private boolean postCloneDoc(long docId, HttpExchange ex, UmbrellaUser user) throws IOException {
Type docType = null;
try {
docType = db.getType(Integer.parseInt(body(ex)));
} catch (NumberFormatException nfe){
throw UmbrellaException.invalidFieldException(BODY,"document type id");
}
Document doc = getDocument(docId, user).a;
var companySettings = db.getCompanySettings(doc.companyId(),docType);
var nextNumber = companySettings.nextDocId();
Document clone = new Document(
0,
doc.companyId(),
nextNumber,
docType,
LocalDate.now(),
NEW,
doc.template(),
doc.delivery(),
doc.head(),
doc.footer(),
doc.currency(),
doc.decimalSeparator(),
doc.sender(),
doc.customer(),
new PositionList()
);
clone = db.save(clone);
doc.positions().values().forEach(clone.positions()::add);
return sendContent(ex,db.save(clone));
}
private boolean postDocument(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
var json = json(ex);
if (!(json.has(SENDER) && json.get(SENDER) instanceof JSONObject senderData)) throw missingFieldException(SENDER);
if (!senderData.has(FIELD_COMPANY) || !(senderData.get(FIELD_COMPANY) instanceof Number companyId)) throw missingFieldException(FIELD_COMPANY);
var company = companyService().get(companyId.longValue());
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are mot a member of company {0}",company);
if (!json.has(FIELD_CUSTOMER) || !(json.get(FIELD_CUSTOMER) instanceof JSONObject customerData)) throw missingFieldException(FIELD_CUSTOMER);
if (!json.has(FIELD_TYPE) || !(json.get(FIELD_TYPE) instanceof Number docTypeId)) throw missingFieldException(FIELD_TYPE);
var type = db.getType(docTypeId.intValue());
var customer = Customer.of(customerData);
Template template = new Template(6,companyId.longValue(),"unknwon",null);
String currency = company.currency();
String sep = company.decimalSeparator();
var settings = db.getCustomerSettings(companyId.longValue(),type,customer.id());
if (settings == null) settings = CustomerSettings.empty();
var companySettings = db.getCompanySettings(companyId.longValue(),type);
var nextNumber = companySettings.nextDocId();
String lastHead = settings.header();
String lastFooter = settings.footer();
var sender = Sender.of(senderData);
LOG.log(DEBUG,json.toString(2));
var doc = new Document(0,companyId.longValue(),nextNumber,type, LocalDate.now(), NEW,template,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList());
var saved = db.save(doc);
db.step(companySettings);
return sendContent(ex,saved);
}
private boolean postDocumentPosition(long docId, HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
var doc = getDocument(docId,user).a;
var json = json(ex);
if (!(json.has(FIELD_AMOUNT) && json.get(FIELD_AMOUNT) instanceof Number amount)) throw missingFieldException(FIELD_AMOUNT);
if (!(json.has(DESCRIPTION) && json.get(DESCRIPTION) instanceof String description)) throw missingFieldException(DESCRIPTION);
if (!(json.has(FIELD_ITEM_CODE) && json.get(FIELD_ITEM_CODE) instanceof String itemCode)) throw missingFieldException(FIELD_ITEM_CODE);
if (!(json.has(TITLE) && json.get(TITLE) instanceof String title)) throw missingFieldException(TITLE);
if (!(json.has(FIELD_UNIT) && json.get(FIELD_UNIT) instanceof String unit)) throw missingFieldException(FIELD_UNIT);
var unitPrice = json.has(FIELD_UNIT_PRICE) && json.get(FIELD_UNIT_PRICE) instanceof Number num ? num : 0L;
try {
unitPrice = db.getCustomerPrice(doc.companyId(),doc.customer().id(),itemCode);
} catch (UmbrellaException ignored) {}
int tax = json.has(FIELD_TAX) && json.get(FIELD_TAX) instanceof Number t ? t.intValue() : 19; // TODO should not be hard-coded
Long timeId = json.has(FIELD_TIME_ID) && json.get(FIELD_TIME_ID) instanceof Number t ? t.longValue() : null;
var pos = new Position(doc.positions().size()+1,itemCode,amount.doubleValue(),unit,title,description,unitPrice.longValue(),tax,timeId,false);
doc.positions().add(pos);
return send(ex,db.save(doc).positions());
}
private boolean postTemplateList(HttpExchange ex, UmbrellaUser user) throws UmbrellaException, IOException {
var json = json(ex);
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY);
var company = companyService().get(companyId.longValue());
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name());
var templates = db.getCompanyTemplates(companyId.longValue());
return sendContent(ex,templates.stream().map(Template::toMap));
}
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException {
var json = json(ex);
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY);
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
var userCompanyIds = companyService().listCompaniesOf(user).keySet();
var documents = db.find(userCompanyIds,keys,fulltext);
return sendContent(ex,mapValues(documents));
}
private boolean postToDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException {
var head = path.pop();
return switch (head){
case CLONE -> postCloneDoc(docId,ex,user);
case POSITION -> postDocumentPosition(docId,ex,user);
case PATH_SEND -> sendDocument(ex,path,user,docId);
case null, default -> super.doPost(path,ex);
};
}
private PriceFormat priceFormat(String currency, String language) {
var pattern = switch (currency){
case "$" -> "$&nbsp;{0,number,#,###.00}";
default -> "{0,number,#,###.00}&nbsp;"+currency;
};
var message = new MessageFormat(pattern, "de".equals(language)? Locale.GERMAN:Locale.US);
return val -> message.format(new Object[]{val/100d});
}
private boolean sendDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException {
var doc = getDocumentWithCompanyData(docId,user);
var rendered = renderDocument(doc,user);
var json = json(ex);
if (!(json.has(EMAIL) && json.get(EMAIL) instanceof String email)) throw missingFieldException(EMAIL);
if (!(json.has(SUBJECT) && json.get(SUBJECT) instanceof String subject)) throw missingFieldException(SUBJECT);
if (!(json.has(CONTENT) && json.get(CONTENT) instanceof String content)) throw missingFieldException(CONTENT);
var settings = new CustomerSettings(doc.head(),doc.footer(),content);
try {
db.save(settings,doc);
} catch (UmbrellaException e) {
LOG.log(WARNING,e);
}
var attachment = new Attachment(doc.number()+".pdf",rendered.mimeType(),rendered.bytes());
var message = new Message(user,subject,content,null,List.of(attachment));
var envelope = new Envelope(message,new User(doc.customer().shortName(),new EmailAddress(email),doc.customer().language()));
postBox().send(envelope);
db.save(doc.set(SENT));
return ok(ex);
}
}