From 4468f45064c8f99548e2b945a6d4f5e7bacb13be Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 31 Oct 2025 22:09:59 +0100 Subject: [PATCH] implemented - assigning new customer numbers to contacts that don`t have one - updating customer number counter in company table Signed-off-by: Stephan Richter --- .../umbrella/company/CompanyModule.java | 28 ++++++++++++++- .../umbrella/company/Constants.java | 1 + .../srsoftware/umbrella/company/SqliteDb.java | 34 +++++++++++++++++++ .../umbrella/company/api/CompanyDb.java | 4 +++ .../umbrella/contact/ContactDb.java | 2 +- .../umbrella/contact/ContactModule.java | 10 ++++++ .../srsoftware/umbrella/contact/SqliteDb.java | 6 ++-- .../umbrella/core/api/CompanyService.java | 2 ++ .../umbrella/core/api/ContactService.java | 4 +++ .../umbrella/core/model/Contact.java | 33 +++++++++++++++++- .../umbrella/documents/Constants.java | 1 + .../umbrella/documents/DocumentApi.java | 27 ++++++++++----- frontend/src/routes/document/Add.svelte | 21 +++++++++--- frontend/src/urls.svelte.js | 4 +++ 14 files changed, 157 insertions(+), 20 deletions(-) diff --git a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java index 9422c17..7f5f133 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java @@ -2,6 +2,7 @@ package de.srsoftware.umbrella.company; import static de.srsoftware.umbrella.company.Constants.CONFIG_DATABASE; +import static de.srsoftware.umbrella.company.Constants.NEXT_CUSTOMER_NUMBER; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.ModuleRegistry.*; @@ -73,7 +74,12 @@ public class CompanyModule extends BaseHandler implements CompanyService { return switch (head){ case LIST -> getCompanyList(user.get(),ex); case null, - default -> super.doGet(path,ex); + default -> { + try { + yield getCompany(user.get(), path, Long.parseLong(head), ex); + } catch (NumberFormatException ignored){} + yield super.doGet(path,ex); + } }; } catch (UmbrellaException e) { return send(ex,e); @@ -124,6 +130,14 @@ public class CompanyModule extends BaseHandler implements CompanyService { return companyDb.load(companyId); } + private boolean getCompany(UmbrellaUser user, Path path, long companyId, HttpExchange ex) throws IOException { + return switch (path.pop()){ + case NEXT_CUSTOMER_NUMBER -> getNextCustomerNumber(user, companyId, ex); + case null, default -> super.doGet(path,ex); + }; + + } + private boolean getCompanyList(UmbrellaUser user, HttpExchange ex) throws IOException, UmbrellaException { var result = new HashMap>(); for (var entry : listCompaniesOf(user).entrySet()) result.put(entry.getKey(),entry.getValue().toMap()); @@ -137,6 +151,13 @@ public class CompanyModule extends BaseHandler implements CompanyService { return members; } + private boolean getNextCustomerNumber(UmbrellaUser user, long companyId, HttpExchange ex) throws IOException { + var company = companyDb.load(companyId); + if (!membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + var nextCustomerNumber = companyDb.getNextCustomerNumber(companyId); + return sendContent(ex,nextCustomerNumber); + } + @Override public Map listCompaniesOf(UmbrellaUser user) throws UmbrellaException { return loadMembers(companyDb.listCompaniesOf(user.id())); @@ -205,4 +226,9 @@ public class CompanyModule extends BaseHandler implements CompanyService { var companies = companyDb.find(user.id(),keys); return sendContent(ex,mapValues(companies)); } + + @Override + public void saveNewCustomer(long companyId, String id) { + companyDb.saveNewCustomer(companyId, id); + } } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/Constants.java b/company/src/main/java/de/srsoftware/umbrella/company/Constants.java index 3b0a524..796ef0a 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/Constants.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/Constants.java @@ -6,6 +6,7 @@ public class Constants { public static final String CONFIG_DATABASE = "umbrella.modules.company.database"; public static final String DISTINCT = "DISTINCT *"; + public static final String NEXT_CUSTOMER_NUMBER = "next_customer_number"; public static final String TABLE_COMPANIES_USERS = "companies_users"; public static final String TABLE_COMPANIES = "companies"; } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java index 5f965e4..58c2842 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java @@ -11,6 +11,7 @@ import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Field.*; import static de.srsoftware.umbrella.core.Field.COMPANY_ID; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.unprocessable; import static java.lang.System.Logger.Level.ERROR; import static java.text.MessageFormat.format; @@ -21,6 +22,7 @@ import de.srsoftware.umbrella.core.model.Company; import java.sql.Connection; import java.sql.SQLException; import java.util.*; +import java.util.regex.Pattern; import org.json.JSONObject; public class SqliteDb extends BaseDb implements CompanyDb { @@ -126,6 +128,24 @@ CREATE TABLE IF NOT EXISTS "companies" ( } } + @Override + public String getNextCustomerNumber(long companyId) { + try { + var rs = select(LAST_CUSTOMER_NUMBER,CUSTOMER_NUMBER_PREFIX).from(TABLE_COMPANIES).where(ID,equal(companyId)).exec(db); + var last = 0L; + String prefix = null; + if (rs.next()){ + last = rs.getLong(LAST_CUSTOMER_NUMBER); + prefix = rs.getString(CUSTOMER_NUMBER_PREFIX); + } + rs.close(); + var next = last+1; + return prefix+next; // TODO: currently not taking growing number lengths into account, this should be resolved! + } catch (SQLException e) { + throw databaseException("Failed to load customer number settings for company {0}",companyId); + } + } + @Override public HashMap find(long userId, Collection keys) { try { @@ -208,4 +228,18 @@ CREATE TABLE IF NOT EXISTS "companies" ( throw UmbrellaException.databaseException("Failed to save {0}…",company.name()); } } + + @Override + public void saveNewCustomer(long companyId, String id) { + var p = Pattern.compile("(?s)(.*?)(\\d+)$"); + var m = p.matcher(id); + if (!m.matches()) throw unprocessable("{0} is not a valid customer id: it does not end with a number!"); + String prefix = m.group(1); // Prefix before last number + long number = Long.parseLong(m.group(2)); // The last numeric part + try { + update(TABLE_COMPANIES).set(LAST_CUSTOMER_NUMBER,CUSTOMER_NUMBER_PREFIX).where(ID,equal(companyId)).prepare(db).apply(number,prefix).close(); + } catch (SQLException e) { + throw databaseException("Failed to update customer number counter for company {0}", companyId); + } + } } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java b/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java index 23972b3..ab4381b 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/api/CompanyDb.java @@ -18,9 +18,13 @@ public interface CompanyDb { Collection getMembers(long companyId) throws UmbrellaException; + String getNextCustomerNumber(long companyId); + Map listCompaniesOf(long id) throws UmbrellaException; Company load(long companyId) throws UmbrellaException; Company save(Company company); + + void saveNewCustomer(long companyId, String id); } diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java index 5ab3744..364695b 100644 --- a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactDb.java @@ -10,7 +10,7 @@ public interface ContactDb { Map listContactsOf(long userId) throws UmbrellaException; - Contact load(long id, long userId); + Contact load(long contactId, long userId); Contact save(Contact contact); diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java index 3345ae7..03245ee 100644 --- a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java @@ -114,6 +114,11 @@ public class ContactModule extends BaseHandler implements ContactService { return sendContent(ex,mapValues(listContactsOf(user))); } + @Override + public Contact load(long userId, long contactId) { + return contactDb.load(contactId,userId); + } + @Override public Map listContactsOf(UmbrellaUser user) throws UmbrellaException { return contactDb.listContactsOf(user.id()); @@ -130,4 +135,9 @@ public class ContactModule extends BaseHandler implements ContactService { contactDb.setOwner(user.id(),contact); return sendContent(ex,contact); } + + @Override + public Contact save(Contact contact) { + return contactDb.save(contact); + } } diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java index d2f8494..46b53c6 100644 --- a/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java @@ -87,14 +87,14 @@ public class SqliteDb extends BaseDb implements ContactDb{ } @Override - public Contact load(long id, long userId) { + public Contact load(long contactId, long userId) { try { - var rs = select(ALL).from(TABLE_CONTACTS).leftJoin(ID,TABLE_CONTACTS_USERS,CONTACT_ID).where(USER_ID,equal(userId)).where(ID,equal(id)).exec(db); + var rs = select(ALL).from(TABLE_CONTACTS).leftJoin(ID,TABLE_CONTACTS_USERS,CONTACT_ID).where(USER_ID,equal(userId)).where(ID,equal(contactId)).exec(db); Contact contact = null; if (rs.next()) contact = Contact.of(rs); rs.close(); if (contact != null) return contact; - throw notFound("Failed to load contact with id = {0}",id); + throw notFound("Failed to load contact with id = {0}", contactId); } catch (SQLException e) { throw databaseException("Failed to load contacts of user {0}",userId); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java index 32aaec4..760729a 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/CompanyService.java @@ -15,4 +15,6 @@ public interface CompanyService { Map listCompaniesOf(UmbrellaUser user) throws UmbrellaException; boolean membership(long companyId, long userId) throws UmbrellaException; + + void saveNewCustomer(long companyId, String id); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java index 12fd921..ef6c542 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/ContactService.java @@ -8,4 +8,8 @@ import java.util.Map; public interface ContactService { Map listContactsOf(UmbrellaUser user) throws UmbrellaException; + + Contact load(long userId, long contactId); + + Contact save(Contact contact); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java index c0c6c54..28b3d0a 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static java.text.MessageFormat.format; import de.srsoftware.tools.Mappable; import java.sql.ResultSet; @@ -10,7 +11,21 @@ import java.sql.SQLException; import java.util.Map; import org.json.JSONObject; -public record Contact(long id, String vcard) implements Mappable { +public class Contact implements Mappable{ + private final static String X_CUSTOMER_NUMBER = "X-CUSTOMER-NUMBER"; + + private long id; + private String vcard; + + public Contact(long id, String vcard){ + this.id = id; + this.vcard = vcard; + } + + public long id(){ + return id; + } + public static Contact of(ResultSet rs) throws SQLException { return new Contact(rs.getLong(ID),rs.getString(DATA)); } @@ -21,8 +36,24 @@ public record Contact(long id, String vcard) implements Mappable { return new Contact(id,vcard.replace(from, to)); } + public Contact setCustomerNumber(String customerNumber) { + if (vcard.contains(X_CUSTOMER_NUMBER)) { + var pattern = format("^{0}:.*",X_CUSTOMER_NUMBER); + var replacement = format("{0}:{1}",X_CUSTOMER_NUMBER,customerNumber); + vcard = vcard.replaceAll(pattern,replacement); + } else { + var index = vcard.lastIndexOf("\n"); + vcard = format("{0}\r\n{1}:{2}{3}",vcard.substring(0,index),X_CUSTOMER_NUMBER,customerNumber,vcard.substring(index)); + } + return this; + } + @Override public Map toMap() { return Map.of(ID,id,VCARD,vcard); } + + public String vcard(){ + return vcard; + } } diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java b/documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java index d3fba89..271424a 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java @@ -15,6 +15,7 @@ public class Constants { public static final String CLONE = "clone"; public static final String CONFIG_DATABASE = "umbrella.modules.document.database"; public static final String CONFIG_TEMPLATES = "umbrella.modules.document.templates"; + public static final String CONTACT_ID = "contact_id"; public static final String CONTACTS = "contacts"; public static final String CUSTOMERS = "customers"; diff --git a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java index 52106da..80657f6 100644 --- a/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java +++ b/documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java @@ -460,28 +460,37 @@ public class DocumentApi extends BaseHandler implements DocumentService { 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(COMPANY) || !(senderData.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 mot a member of company {0}",company); + if (!senderData.has(COMPANY) || !(senderData.get(COMPANY) instanceof Number rawCompId)) throw missingFieldException(COMPANY); + var companyId = rawCompId.longValue(); + var company = companyService().get(companyId); + if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company); if (!json.has(CUSTOMER) || !(json.get(CUSTOMER) instanceof JSONObject customerData)) throw missingFieldException(CUSTOMER); if (!json.has(TYPE) || !(json.get(TYPE) instanceof Number docTypeId)) throw missingFieldException(TYPE); var type = db.getType(docTypeId.intValue()); var customer = Customer.of(customerData); - Template template = new Template(6,companyId.longValue(),"unknwon",null); + Template template = new Template(6,companyId,"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 settings = db.getCustomerSettings(companyId,type,customer.id()); + var newCustomer = settings == null; + if (newCustomer) settings = CustomerSettings.empty(); + var companySettings = db.getCompanySettings(companyId,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 doc = new Document(0,companyId,nextNumber,type, LocalDate.now(), NEW,template,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList()); var saved = db.save(doc); + if (newCustomer) { + if (customerData.get(CONTACT_ID) instanceof Number contactId) { + var contacts = contactService(); + var contact = contacts.load(user.id(), contactId.longValue()); + contacts.save(contact.setCustomerNumber(customer.id())); + } + companyService().saveNewCustomer(companyId,customer.id()); + } db.step(companySettings); return sendContent(ex,saved); } diff --git a/frontend/src/routes/document/Add.svelte b/frontend/src/routes/document/Add.svelte index a83608c..2c0b3fb 100644 --- a/frontend/src/routes/document/Add.svelte +++ b/frontend/src/routes/document/Add.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { useTinyRouter } from 'svelte-tiny-router'; - import { api } from '../../urls.svelte'; + import { api, get } from '../../urls.svelte'; import { error, yikes } from '../../warn.svelte'; import { t } from '../../translations.svelte'; @@ -67,10 +67,21 @@ if (contact.ADR.street) addr += contact.ADR.street+"\n"; if (contact.ADR.locality) addr += contact.ADR.post_code + " "+ contact.ADR.locality + "\n"; if (contact.ADR.county) addr += contact.ADR.country+"\n"; - document.customer.name = addr; - document.customer.tax_id = contact.tax_id; - document.customer.id = contact.customer_number; - document.customer.email = contact.EMAIL; + document.customer.name = addr; + document.customer.tax_id = contact.tax_id; + document.customer.id = contact.customer_number; + document.customer.email = contact.EMAIL; + document.customer.contact_id = contact.id; + if (!document.customer.id) requestNewCustomerId(); + } + + async function requestNewCustomerId(){ + var url = api(`company/${document.sender.company}/next_customer_number`); + var res = await get(url); + yikes(); + document.customer.id = await res.text(); + if (res.ok){ + } else error(res); } async function submit(){ diff --git a/frontend/src/urls.svelte.js b/frontend/src/urls.svelte.js index e34008a..145d8ee 100644 --- a/frontend/src/urls.svelte.js +++ b/frontend/src/urls.svelte.js @@ -4,6 +4,10 @@ export function api(rel_path){ return `${location.protocol}//${location.host.replace('5173','8080')}/api/${rel_path}`; } +export function get(url){ + return fetch(url,{ credentials:'include' }); +} + export function drop(url){ return fetch(url,{ credentials:'include',