Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions samples/bookshop/app/supplier-invoices/fiori-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ annotate service.SupplierInvoices with @(
]
);

annotate service.SupplierInvoices with {
invoiceNumber @title: '{i18n>InvoiceNumber}';
supplier @title: '{i18n>Supplier}';
status @title: '{i18n>Status}';
}
Comment on lines +70 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best Practices: The new @title annotations for invoiceNumber, supplier, and status duplicate labels already defined inline on every LineItem and FieldGroup entry in the same file.

@title on a property acts as a default label and is useful when no explicit Label is provided. Since all existing LineItem and FieldGroup data fields already carry explicit Label keys that match these titles, this block adds no functional value and creates a maintenance risk (two places to update labels). Consider removing the block or replacing the per-field Label: properties with this single source of truth instead.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


annotate service.SupplierInvoices with {
supplier @Common: {
Text : supplier.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ b1e2c3d4-5f6a-7b8c-9d0e-1f2a3b4c5d6e;Springer Nature;DE;orders@springernature.co
c2f3d4e5-6a7b-8c9d-0e1f-2a3b4c5d6e7f;O'Reilly Media;US;invoices@oreilly.com
d3a4b5c6-7d8e-9f0a-1b2c-3d4e5f6a7b8c;Elsevier;NL;ap@elsevier.com
e4b5c6d7-8e9f-0a1b-2c3d-4e5f6a7b8c9d;Pearson Education;GB;finance@pearson.com
e4b5c6d7-8e9f-0a1b-2c3d-4e5f6a7b8c9f;Studio Salford;US;@pearson.com

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invalid email address for new supplier entry

The email value @pearson.com is missing the local part before the @ sign, making it an invalid email address. This will likely cause validation errors or be rejected by email-handling logic.

Consider providing a valid email address such as ap@studiosalford.com.

Suggested change
e4b5c6d7-8e9f-0a1b-2c3d-4e5f6a7b8c9f;Studio Salford;US;@pearson.com
e4b5c6d7-8e9f-0a1b-2c3d-4e5f6a7b8c9f;Studio Salford;US;ap@studiosalford.com

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

4 changes: 4 additions & 0 deletions samples/bookshop/db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ entity SupplierInvoices : managed, cuid {
status : Association to InvoiceStatus;
documentAiJobId : String;
}

annotate SupplierInvoices with {
status @UI.RecommendationState: 0;
Comment thread
lisajulia marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion samples/bookshop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<cds.services.version>5.0.0</cds.services.version>
<spring.boot.version>4.1.0</spring.boot.version>
<cds-starter-ai.version>0.0.1-alpha</cds-starter-ai.version>
<cds-feature-sap-document-ai.version>0.0.1-alpha</cds-feature-sap-document-ai.version>
<cds-feature-sap-document-ai.version>0.0.2-alpha</cds-feature-sap-document-ai.version>
<cds-feature-attachments.version>1.5.0</cds-feature-attachments.version>

</properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import cds.gen.sap.capire.bookshop.SupplierInvoices;
import cds.gen.sap.capire.bookshop.SupplierInvoices_;
import cds.gen.sap.capire.bookshop.Suppliers;
import cds.gen.sap.capire.bookshop.Suppliers_;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionResult;
import com.sap.cds.feature.documentai.generated.cds4j.sap.document.ai.documentaiservice.DocumentExtractionResultContext;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.services.cds.ApplicationService;
import com.sap.cds.services.handler.EventHandler;
Expand All @@ -17,7 +20,9 @@
import com.sap.cds.services.persistence.PersistenceService;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -26,7 +31,7 @@
/**
* Handles extraction results from the SAP Document AI plugin. Parses the extraction JSON returned
* by the DIE service and populates the corresponding SupplierInvoice entity with the extracted
* header fields (invoice number, date, total amount, currency).
* header fields (invoice number, date, total amount, currency, and matched supplier).
*/
@Component
@ServiceName(value = "*", type = ApplicationService.class)
Expand All @@ -50,18 +55,15 @@ public void onExtractionCompleted(DocumentExtractionResultContext context) {
JsonNode root = objectMapper.readTree(data.getExtractionResult());
JsonNode headerFields = root.path("extraction").path("headerFields");

// Extract relevant header fields from the DIE response
String documentNumber = getHeaderFieldValue(headerFields, "documentNumber");
String documentDate = getHeaderFieldValue(headerFields, "documentDate");
String currencyCode = getHeaderFieldValue(headerFields, "currencyCode");
BigDecimal grossAmount = getHeaderFieldNumber(headerFields, "grossAmount");
String senderName = getHeaderFieldValue(headerFields, "senderName");

logger.info(
"[bookshop] Extracted: number={}, date={}, amount={} {}",
documentNumber,
documentDate,
grossAmount,
currencyCode);
"[bookshop] Extracted: number={}, date={}, amount={} {}, sender={}",
documentNumber, documentDate, grossAmount, currencyCode, senderName);

// Update the invoice that is currently in EXTRACTING status.
// In a production app you would correlate by a stored jobId; for this sample
Expand All @@ -73,6 +75,18 @@ public void onExtractionCompleted(DocumentExtractionResultContext context) {
if (grossAmount != null) updateData.put(SupplierInvoices.TOTAL_AMOUNT, grossAmount);
if (currencyCode != null) updateData.put(SupplierInvoices.CURRENCY_CODE, currencyCode);

// Match extracted sender name to a Supplier record (case-insensitive)
if (senderName != null) {
findSupplierByName(senderName)
.ifPresentOrElse(
supplier -> {
updateData.put(SupplierInvoices.SUPPLIER_ID, supplier.getId());
logger.info(
"[bookshop] Matched supplier: {} ({})", supplier.getName(), supplier.getId());
},
() -> logger.warn("[bookshop] No supplier found for sender name: {}", senderName));
}

long updated =
db.run(
Update.entity(SupplierInvoices_.CDS_NAME)
Expand All @@ -93,6 +107,15 @@ public void onExtractionCompleted(DocumentExtractionResultContext context) {
context.setCompleted();
}

private Optional<Suppliers> findSupplierByName(String name) {
String normalized = name.toLowerCase(Locale.ROOT);
return db.run(Select.from(Suppliers_.class))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: findSupplierByName fetches the entire Suppliers table into memory to do a case-insensitive name match in Java.

For even a modest number of suppliers this is wasteful — every extraction event loads all supplier rows. The filter should be pushed down to the database using a where clause with a lower-case comparison, or at minimum a where(s -> s.name().eq(name)) combined with post-filter. Consider:

return db.run(Select.from(Suppliers_.class)
        .where(s -> s.name().eq(name)))
    .listOf(Suppliers.class)
    .stream()
    .filter(s -> s.getName() != null && normalized.equals(s.getName().toLowerCase(Locale.ROOT)))
    .findFirst();

This at least avoids a full-table scan for exact matches and falls back to the in-memory case-insensitive check only when needed.

Suggested change
return db.run(Select.from(Suppliers_.class))
return db.run(Select.from(Suppliers_.class)
.where(s -> s.name().eq(name)))
.listOf(Suppliers.class)
.stream()
.filter(s -> s.getName() != null && normalized.equals(s.getName().toLowerCase(Locale.ROOT)))
.findFirst();

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

.listOf(Suppliers.class)
.stream()
.filter(s -> s.getName() != null && normalized.equals(s.getName().toLowerCase(Locale.ROOT)))
.findFirst();
}

private String getHeaderFieldValue(JsonNode headerFields, String fieldName) {
for (JsonNode field : headerFields) {
if (fieldName.equals(field.path("name").asText())) {
Expand Down
Loading