Skip to content

Commit a52722b

Browse files
frtrincaangelosca24skymoronigu
authored
feat: [EBE-450] Hotfix update invoice file and number (#240)
Co-authored-by: angelosca24 <angelo.scagliola@nttdata.com> Co-authored-by: EMORONGD6 <guido.moroni@emeal.nttdata.com>
1 parent 67b399e commit a52722b

File tree

12 files changed

+422
-6
lines changed

12 files changed

+422
-6
lines changed

src/main/java/it/gov/pagopa/idpay/transactions/controller/PointOfSaleTransactionController.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import org.springframework.data.domain.Pageable;
1212
import org.springframework.data.domain.Sort;
1313
import org.springframework.data.web.PageableDefault;
14+
import org.springframework.http.HttpStatus;
1415
import org.springframework.http.MediaType;
16+
import org.springframework.http.codec.multipart.FilePart;
1517
import org.springframework.web.bind.annotation.*;
1618
import reactor.core.publisher.Mono;
1719

@@ -67,4 +69,14 @@ Mono<DownloadInvoiceResponseDTO> downloadInvoiceFile(
6769
@RequestHeader("x-merchant-id") String merchantId,
6870
@PathVariable("pointOfSaleId") String pointOfSaleId,
6971
@PathVariable("transactionId") String transactionId);
72+
73+
@PutMapping(path = "/transactions/{transactionId}/invoice/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
74+
@ResponseStatus(code = HttpStatus.NO_CONTENT)
75+
Mono<Void> updateInvoiceFile(
76+
@PathVariable("transactionId") String transactionId,
77+
@RequestHeader("x-merchant-id") String merchantId,
78+
@RequestHeader("x-point-of-sale-id") String pointOfSaleId,
79+
@RequestPart("file") FilePart file,
80+
@RequestPart(value = "docNumber", required = false) String docNumber
81+
);
7082
}

src/main/java/it/gov/pagopa/idpay/transactions/controller/PointOfSaleTransactionControllerImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import it.gov.pagopa.idpay.transactions.utils.Utilities;
88
import lombok.extern.slf4j.Slf4j;
99
import org.springframework.data.domain.Pageable;
10+
import org.springframework.http.codec.multipart.FilePart;
1011
import org.springframework.web.bind.annotation.RestController;
12+
import org.springframework.web.multipart.MultipartFile;
1113
import reactor.core.publisher.Flux;
1214
import reactor.core.publisher.Mono;
1315

@@ -49,4 +51,19 @@ public Mono<DownloadInvoiceResponseDTO> downloadInvoiceFile(
4951
Utilities.sanitizeString(transactionId));
5052
return pointOfSaleTransactionService.downloadTransactionInvoice(merchantId, pointOfSaleId, transactionId);
5153
}
54+
55+
@Override
56+
public Mono<Void> updateInvoiceFile(String transactionId, String merchantId, String pointOfSaleId,
57+
FilePart file, String docNumber) {
58+
final String sanitizedMerchantId = Utilities.sanitizeString(merchantId);
59+
final String sanitizedTrxCode = Utilities.sanitizeString(transactionId);
60+
final String sanitizedPointOfSaleId = Utilities.sanitizeString(pointOfSaleId);
61+
62+
log.info(
63+
"[UPDATE_INVOICE_TRANSACTION] The merchant {} is requesting a invoice update for the transactionId {} at POS {}",
64+
sanitizedMerchantId, sanitizedTrxCode, sanitizedPointOfSaleId
65+
);
66+
return pointOfSaleTransactionService.updateInvoiceTransaction(transactionId, merchantId,
67+
pointOfSaleId, file, docNumber);
68+
}
5269
}

src/main/java/it/gov/pagopa/idpay/transactions/dto/RewardTransactionDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@ public class RewardTransactionDTO {
6969
private InvoiceData invoiceData;
7070
private InvoiceData creditNoteData;
7171
private String trxCode;
72+
private LocalDateTime updateDate;
7273
}

src/main/java/it/gov/pagopa/idpay/transactions/dto/mapper/RewardTransactionMapper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package it.gov.pagopa.idpay.transactions.dto.mapper;
22

33
import it.gov.pagopa.idpay.transactions.dto.RewardTransactionDTO;
4+
import it.gov.pagopa.idpay.transactions.enums.SyncTrxStatus;
45
import it.gov.pagopa.idpay.transactions.model.RewardTransaction;
56
import org.apache.commons.lang3.StringUtils;
67
import org.springframework.stereotype.Service;
@@ -66,6 +67,11 @@ public RewardTransaction mapFromDTO(RewardTransactionDTO rewardTrxDto) {
6667
rewardTrx.setInvoiceData(rewardTrxDto.getInvoiceData());
6768
rewardTrx.setCreditNoteData(rewardTrxDto.getCreditNoteData());
6869
rewardTrx.setTrxCode((rewardTrxDto.getTrxCode()));
70+
if(SyncTrxStatus.INVOICED.name().equals(rewardTrxDto.getStatus())){
71+
rewardTrx.setInvoiceUploadDate(rewardTrxDto.getUpdateDate());
72+
}
73+
74+
6975
}
7076

7177
return rewardTrx;

src/main/java/it/gov/pagopa/idpay/transactions/model/RewardTransaction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ public class RewardTransaction {
6565
private InvoiceData invoiceData;
6666
private InvoiceData creditNoteData;
6767
private String trxCode;
68+
private LocalDateTime invoiceUploadDate;
6869
}

src/main/java/it/gov/pagopa/idpay/transactions/service/PointOfSaleTransactionService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import it.gov.pagopa.idpay.transactions.model.RewardTransaction;
55
import org.springframework.data.domain.Page;
66
import org.springframework.data.domain.Pageable;
7+
import org.springframework.http.codec.multipart.FilePart;
78
import reactor.core.publisher.Mono;
89

910
public interface PointOfSaleTransactionService {
1011

1112
Mono<Page<RewardTransaction>> getPointOfSaleTransactions(String merchantId, String initiativeId, String pointOfSaleId, String productGtin, String fiscalCode, String status, Pageable pageable);
1213

1314
Mono<DownloadInvoiceResponseDTO> downloadTransactionInvoice(String merchantId, String pointOfSaleId, String transactionId);
15+
16+
Mono<Void> updateInvoiceTransaction(String transactionId, String merchantId, String pointOfSaleId, FilePart file, String docNumber);
1417
}

src/main/java/it/gov/pagopa/idpay/transactions/service/PointOfSaleTransactionServiceImpl.java

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package it.gov.pagopa.idpay.transactions.service;
22

33
import it.gov.pagopa.common.web.exception.ClientExceptionNoBody;
4+
import it.gov.pagopa.common.web.exception.ClientExceptionWithBody;
45
import it.gov.pagopa.idpay.transactions.connector.rest.UserRestClient;
56
import it.gov.pagopa.idpay.transactions.connector.rest.dto.FiscalCodeInfoPDV;
67
import it.gov.pagopa.idpay.transactions.dto.DownloadInvoiceResponseDTO;
@@ -9,15 +10,24 @@
910
import it.gov.pagopa.idpay.transactions.model.RewardTransaction;
1011
import it.gov.pagopa.idpay.transactions.repository.RewardTransactionRepository;
1112
import it.gov.pagopa.idpay.transactions.storage.InvoiceStorageClient;
13+
import it.gov.pagopa.idpay.transactions.utils.ExceptionConstants;
14+
import it.gov.pagopa.idpay.transactions.utils.Utilities;
1215
import lombok.extern.slf4j.Slf4j;
1316
import org.apache.commons.lang3.StringUtils;
1417
import org.springframework.data.domain.Page;
1518
import org.springframework.data.domain.PageImpl;
1619
import org.springframework.data.domain.Pageable;
1720
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.codec.multipart.FilePart;
1822
import org.springframework.stereotype.Service;
1923
import reactor.core.publisher.Mono;
2024

25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
2131
import static it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionMessage.TRANSACTION_MISSING_INVOICE;
2232

2333
@Service
@@ -29,7 +39,7 @@ public class PointOfSaleTransactionServiceImpl implements PointOfSaleTransaction
2939
private final InvoiceStorageClient invoiceStorageClient;
3040

3141
protected PointOfSaleTransactionServiceImpl(
32-
UserRestClient userRestClient, RewardTransactionRepository rewardTransactionRepository, InvoiceStorageClient invoiceStorageClient) {
42+
UserRestClient userRestClient, RewardTransactionRepository rewardTransactionRepository, InvoiceStorageClient invoiceStorageClient) {
3343
this.userRestClient = userRestClient;
3444
this.rewardTransactionRepository = rewardTransactionRepository;
3545
this.invoiceStorageClient = invoiceStorageClient;
@@ -91,7 +101,79 @@ public Mono<DownloadInvoiceResponseDTO> downloadTransactionInvoice(
91101
});
92102
}
93103

94-
private Mono<Page<RewardTransaction>> getTransactions(String merchantId, String initiativeId, String pointOfSaleId, String userId, String productGtin, String status, Pageable pageable) {
104+
@Override
105+
public Mono<Void> updateInvoiceTransaction(String transactionId, String merchantId, String pointOfSaleId, FilePart file, String docNumber) {
106+
try {
107+
Utilities.checkFileExtensionOrThrow(file);
108+
109+
return rewardTransactionRepository.findTransaction(merchantId, pointOfSaleId, transactionId)
110+
.switchIfEmpty(Mono.error(new ClientExceptionNoBody(HttpStatus.BAD_REQUEST, TRANSACTION_MISSING_INVOICE)))
111+
.flatMap(rewardTransaction -> {
112+
String status = rewardTransaction.getStatus();
113+
InvoiceData oldDocumentData = null;
114+
115+
if (SyncTrxStatus.INVOICED.name().equalsIgnoreCase(status)) {
116+
oldDocumentData = rewardTransaction.getInvoiceData();
117+
} else {
118+
throw new ClientExceptionNoBody(HttpStatus.BAD_REQUEST, TRANSACTION_MISSING_INVOICE);
119+
}
120+
if (!rewardTransaction.getMerchantId().equals(merchantId)) {
121+
throw new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, ExceptionConstants.ExceptionCode.GENERIC_ERROR,
122+
"The merchant with id [%s] associated to the transaction is not equal to the merchant with id [%s]".formatted(
123+
rewardTransaction.getMerchantId(), merchantId));
124+
}
125+
if (!rewardTransaction.getPointOfSaleId().equals(pointOfSaleId)) {
126+
throw new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, ExceptionConstants.ExceptionCode.GENERIC_ERROR,
127+
"The pointOfSaleId with id [%s] associated to the transaction is not equal to the pointOfSaleId with id [%s]".formatted(
128+
rewardTransaction.getPointOfSaleId(), pointOfSaleId));
129+
}
130+
131+
String oldFilename = oldDocumentData.getFilename();
132+
133+
String blobPath = String.format(
134+
"invoices/merchant/%s/pos/%s/transaction/%s/invoice/%s",
135+
merchantId, pointOfSaleId, transactionId, file.filename());
136+
String oldBlobPath = String.format(
137+
"invoices/merchant/%s/pos/%s/transaction/%s/invoice/%s",
138+
merchantId, pointOfSaleId, transactionId, oldFilename);
139+
Path tempPath = Paths.get(System.getProperty("java.io.tmpdir"), file.filename());
140+
141+
return file.transferTo(tempPath)
142+
.then(Mono.fromCallable(() -> {
143+
// Delete old file from storage
144+
invoiceStorageClient.deleteFile(oldBlobPath);
145+
146+
// Upload new file on storage
147+
try (InputStream is = Files.newInputStream(tempPath)) {
148+
String contentType = file.headers().getContentType() != null
149+
? file.headers().getContentType().toString()
150+
: null;
151+
invoiceStorageClient.upload(is, blobPath, contentType);
152+
}
153+
154+
return null;
155+
}))
156+
.onErrorMap(IOException.class, e -> {
157+
log.error("Error uploading file to storage for transaction [{}]", Utilities.sanitizeString(transactionId), e);
158+
throw new ClientExceptionWithBody(HttpStatus.INTERNAL_SERVER_ERROR,
159+
ExceptionConstants.ExceptionCode.GENERIC_ERROR, "Error uploading invoice file", e);
160+
})
161+
.then(Mono.fromRunnable(() ->
162+
// Update Transaction document
163+
rewardTransaction.setInvoiceData(InvoiceData.builder()
164+
.filename(file.filename())
165+
.docNumber(docNumber)
166+
.build())
167+
))
168+
.then(rewardTransactionRepository.save(rewardTransaction).then());
169+
});
170+
} catch (Exception e) {
171+
throw new ClientExceptionWithBody(HttpStatus.INTERNAL_SERVER_ERROR,
172+
ExceptionConstants.ExceptionCode.GENERIC_ERROR, "Error uploading invoice file", e);
173+
}
174+
}
175+
176+
private Mono<Page<RewardTransaction>> getTransactions(String merchantId, String initiativeId, String pointOfSaleId, String userId, String productGtin, String status, Pageable pageable) {
95177
return rewardTransactionRepository.findByFilterTrx(merchantId, initiativeId, pointOfSaleId, userId, productGtin, status, pageable)
96178
.collectList()
97179
.zipWith(rewardTransactionRepository.getCount(merchantId, initiativeId, pointOfSaleId, userId, productGtin, status))

src/main/java/it/gov/pagopa/idpay/transactions/storage/InvoiceStorageClient.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package it.gov.pagopa.idpay.transactions.storage;
22

3+
import com.azure.core.http.rest.Response;
34
import com.azure.storage.blob.BlobClient;
45
import com.azure.storage.blob.BlobContainerClient;
56
import com.azure.storage.blob.BlobServiceClient;
67
import com.azure.storage.blob.models.BlobStorageException;
8+
import com.azure.storage.blob.models.BlockBlobItem;
9+
import com.azure.storage.blob.models.DeleteSnapshotsOptionType;
710
import com.azure.storage.blob.models.UserDelegationKey;
11+
import com.azure.storage.blob.options.BlobParallelUploadOptions;
12+
import com.azure.storage.blob.options.BlobUploadFromFileOptions;
813
import com.azure.storage.blob.sas.BlobSasPermission;
914
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
1015
import it.gov.pagopa.common.web.exception.ClientException;
16+
import it.gov.pagopa.idpay.transactions.utils.Utilities;
17+
import java.io.File;
18+
import java.io.InputStream;
1119
import lombok.extern.slf4j.Slf4j;
1220
import org.apache.commons.lang3.StringUtils;
1321
import org.springframework.http.HttpStatus;
@@ -68,4 +76,18 @@ public String getFileSignedUrl(String blobPath) {
6876
HttpStatus.INTERNAL_SERVER_ERROR, ERROR_ON_GET_FILE_URL_REQUEST, blobStorageException);
6977
}
7078
}
79+
80+
public Response<BlockBlobItem> upload(InputStream inputStream, String destination, String contentType) {
81+
log.info("Uploading (contentType={}) into azure blob at destination {}", Utilities.sanitizeString(contentType), Utilities.sanitizeString(destination));
82+
83+
return blobContainerClient.getBlobClient(destination)
84+
.uploadWithResponse(new BlobParallelUploadOptions(inputStream), null, null);
85+
}
86+
87+
public Response<Boolean> deleteFile(String destination) {
88+
log.info("Deleting file {} from azure blob container", Utilities.sanitizeString(destination));
89+
90+
return blobContainerClient.getBlobClient(destination)
91+
.deleteIfExistsWithResponse(DeleteSnapshotsOptionType.INCLUDE, null, null, null);
92+
}
7193
}

src/main/java/it/gov/pagopa/idpay/transactions/utils/Utilities.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package it.gov.pagopa.idpay.transactions.utils;
22

3+
import it.gov.pagopa.common.web.exception.ClientExceptionWithBody;
4+
import it.gov.pagopa.idpay.transactions.utils.ExceptionConstants.ExceptionCode;
35
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.codec.multipart.FilePart;
48

59
@Slf4j
610
public class Utilities {
@@ -10,4 +14,16 @@ private Utilities() {}
1014
public static String sanitizeString(String str){
1115
return str.replaceAll("[\\r\\n]", "").replaceAll("[^\\w\\s-]", "");
1216
}
17+
18+
public static void checkFileExtensionOrThrow(FilePart file) {
19+
if (file == null) {
20+
throw new ClientExceptionWithBody(HttpStatus.BAD_REQUEST, ExceptionCode.GENERIC_ERROR, "File is required");
21+
}
22+
23+
String filename = file.filename();
24+
if (!filename.toLowerCase().endsWith(".pdf") && !filename.toLowerCase().endsWith(".xml")) {
25+
throw new ClientExceptionWithBody(HttpStatus.BAD_REQUEST,
26+
ExceptionCode.GENERIC_ERROR, "File must be a PDF or XML");
27+
}
28+
}
1329
}

src/test/java/it/gov/pagopa/idpay/transactions/dto/mapper/RewardTransactionMapperTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ void mapFromDTOTransaction() {
4141
assertCommonFields(resultRefund, refundTrx);
4242
checkNotNullRewardField(resultRefund.getRewards());
4343
assertRefundFields(resultRefund,refundTrx);
44-
TestUtils.checkNotNullFields(resultRefund, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode");
44+
TestUtils.checkNotNullFields(resultRefund, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode", "invoiceUploadDate");
4545

4646
Assertions.assertNotNull(resultRejected);
4747
assertCommonFields(resultRejected, rejectedTrx);
4848
assertRejectedFields(resultRejected,rejectedTrx);
49-
TestUtils.checkNotNullFields(resultRejected, "initiatives","rewards", "operationTypeTranscoded", "effectiveAmountCents","trxChargeDate","refundInfo", "additionalProperties", "invoiceData", "creditNoteData", "trxCode");
49+
TestUtils.checkNotNullFields(resultRejected, "initiatives","rewards", "operationTypeTranscoded", "effectiveAmountCents","trxChargeDate","refundInfo", "additionalProperties", "invoiceData", "creditNoteData", "trxCode", "invoiceUploadDate");
5050

5151

5252
}
@@ -63,7 +63,7 @@ void mapFromDTOTransactionWithoutId(){
6363
//Then
6464
Assertions.assertNotNull(result);
6565
assertCommonFields(result, rewardTrx);
66-
TestUtils.checkNotNullFields(result, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode");
66+
TestUtils.checkNotNullFields(result, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode", "invoiceUploadDate");
6767

6868
String expectedId = rewardTrx.getIdTrxAcquirer()
6969
.concat(rewardTrx.getAcquirerCode())
@@ -87,7 +87,7 @@ void mapFromDTOTransactionWithRefund() {
8787
Assertions.assertNotNull(result);
8888
assertCommonFields(result, rewardTrx);
8989

90-
TestUtils.checkNotNullFields(result, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode");
90+
TestUtils.checkNotNullFields(result, "rejectionReasons", "initiativeRejectionReasons", "additionalProperties", "invoiceData", "creditNoteData", "trxCode", "invoiceUploadDate");
9191
checkNotNullRewardField(result.getRewards());
9292
TestUtils.checkNotNullFields(result.getRefundInfo());
9393
}

0 commit comments

Comments
 (0)