diff --git a/docs/swagger/api.yaml b/docs/swagger/api.yaml index 9861d2a..bdb7cd0 100644 --- a/docs/swagger/api.yaml +++ b/docs/swagger/api.yaml @@ -109,7 +109,90 @@ paths: Problem occured while printing the receipt schema: $ref: "#/definitions/RequestTimeoutError" + + /v2/receipts/print: + post: + summary: Print Receipts + parameters: + - in: body + name: request + description: Prining receipts and fiscal receipts + required: true + schema: + $ref: '#/definitions/PrintReceiptRequest' + description: Saving receipts to be pritned asynchronously + responses: + 200: + description: Receipt saved and queued to be printed + schema: + type: string + example: "receiptId" + 400: + description: + The requested receipt is already saved + + /v2/receipts/{id}/status: + get: + summary: Get receipt status + parameters: + - in: path + name: id + type: string + required: true + description: The id of the receipt + responses: + 200: + description: Receipt found + schema: + type: string + example: "PRINTING" + 404: + description: Receipt not found + /v2/receipts/latest/{limit}: + get: + summary: Gets latest receipts up to a limit + parameters: + - in: path + name: limit + type: integer + format: int32 + required: true + description: The limit to return + responses: + 200: + description: Receipts retrieved + schema: + type: object + properties: + printStatus: + type: string + requestId: + type: string + operatorId: + type: string + sourceIp: + type: string + isFiscal: + type: boolean + receipt: + $ref: '#/definitions/Receipt' + + /v2/receipts/{requestId}/requeue: + post: + summary: Requeues a receipt + parameters: + - in: path + name: requestId + type: string + required: true + description: The request id of the receipt + responses: + 200: + description: The receipt was requeued for printing + 404: + description: The receipt has not been registered + /v1/reports/operator/print: post: summary: Prints Report @@ -194,6 +277,10 @@ definitions: PrintReceiptRequest: type: object properties: + receiptId: + type: string + description: Unique id representing the receipt + example: "receiptId" sourceIp: type: string description: Unique ip representing the owner of the device. @@ -206,53 +293,60 @@ definitions: type: boolean description: true means fiscal receipt, false means text receipt: - type: object - description: Represents receipt item - properties: - receiptId: - type: string - description: Receipt ID - example: "123" - currency: - type: string - description: Currency - example: "USD" - prefixLines: - type: array - items: - type: string - suffixLines: - type: array - items: - type: string - amount: - type: number - format: double - description: amount to be paid - example: 1.0 - receiptItems: - type: object - description: Represents receipt item - properties: - name: - type: string - description: Name of the item - example: - quantity: - type: number - format: double - description: Quantity of the item - example: 2.0 - price: - type: number - format: double - description: Price of the item (with vat) - example: 1.0 - vat: - type: number - format: double - description: VAT - example: 20.0 + $ref: '#/definitions/Receipt' + Receipt: + type: object + description: Represents receipt item + properties: + receiptId: + type: string + description: Receipt ID + example: "123" + currency: + type: string + description: Currency + example: "USD" + prefixLines: + type: array + items: + type: string + suffixLines: + type: array + items: + type: string + amount: + type: number + format: double + description: amount to be paid + example: 1.0 + receiptItems: + type: array + items: + $ref: '#/definitions/ReceiptItem' + ReceiptItem: + type: object + description: Represents receipt item + properties: + name: + type: string + description: Name of the item + example: + quantity: + type: number + format: double + description: Quantity of the item + example: 2.0 + price: + type: number + format: double + description: Price of the item (with vat) + example: 1.0 + vat: + type: number + format: double + description: VAT + example: 20.0 + PrintReceiptResponse: type: object properties: @@ -352,4 +446,4 @@ definitions: properties: message: type: string - example: 'Printer request timeout. Unable to get response after 50 retries' \ No newline at end of file + example: 'Printer request timeout. Unable to get response after 50 retries' diff --git a/pos-print/build.gradle b/pos-print/build.gradle index b2f8ec6..fdd35c4 100644 --- a/pos-print/build.gradle +++ b/pos-print/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.1.1' + ext.kotlin_version = '1.2.71' repositories { mavenCentral() jcenter() @@ -51,6 +51,9 @@ dependencies { compile 'com.google.inject.extensions:guice-servlet:3.0' compile 'com.google.guava:guava:19.0' + //Apache commons + compile 'commons-io:commons-io:2.5' + // Internal version of Sitebricks compile('org.mvel:mvel2:2.1.3.Final') compile('org.jsoup:jsoup:1.8.1') @@ -83,11 +86,14 @@ dependencies { compile 'org.slf4j:slf4j-api:1.6.3' compile 'ch.qos.logback:logback-classic:0.9.30' + //GuavaServices + compile group: 'com.google.guava', name: 'guava', version: '19.0' + //MongoDatabase - compile 'org.mongodb:mongodb-driver:3.4.2' + compile 'org.mongodb:mongodb-driver:3.6.4' //FongoDatabase - compile 'com.github.fakemongo:fongo:2.0.9' + compile 'com.github.fakemongo:fongo:2.2.0-RC2' compile 'com.github.spullara.cli-parser:cli-parser:1.1.2' compile 'com.google.code.gson:gson:2.8.0' diff --git a/pos-print/src/main/java/com/clouway/pos/print/PosPrintService.java b/pos-print/src/main/java/com/clouway/pos/print/PosPrintService.java index 0fcee30..094e23d 100644 --- a/pos-print/src/main/java/com/clouway/pos/print/PosPrintService.java +++ b/pos-print/src/main/java/com/clouway/pos/print/PosPrintService.java @@ -1,5 +1,6 @@ package com.clouway.pos.print; +import com.clouway.pos.print.core.BackgroundReceiptPrintingService; import com.clouway.pos.print.adapter.http.HttpBackend; import com.clouway.pos.print.adapter.http.HttpModule; import com.clouway.pos.print.core.CoreModule; @@ -39,15 +40,20 @@ public static void main(String[] args) { new PersistentModule(client, commandCLI.dbName()) ); + HttpBackend backend = new HttpBackend(commandCLI.httpPort(), injector); backend.start(); + BackgroundReceiptPrintingService backgroundPrinter = injector.getInstance(BackgroundReceiptPrintingService.class); + backgroundPrinter.startAsync().awaitRunning(); + System.out.printf("POS Print Service is up and running on port: %d\n", commandCLI.httpPort()); Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("POS Print Service is going to shutdown."); try { backend.stop(); + backgroundPrinter.stopAsync(); } catch (Exception e) { System.out.println("Failed to stop server due: " + e.getMessage()); } diff --git a/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentModule.kt b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentModule.kt index 408e824..a432290 100644 --- a/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentModule.kt +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentModule.kt @@ -1,5 +1,8 @@ package com.clouway.pos.print.adapter.db +import com.clouway.pos.print.core.IdGenerator +import com.clouway.pos.print.core.ReceiptRepository +import com.clouway.pos.print.core.SimpleUUIDGenerator import com.google.inject.AbstractModule import com.google.inject.Provides import com.google.inject.Singleton @@ -13,6 +16,8 @@ class PersistentModule(private val client: MongoClient, private val databaseName override fun configure() { bind(CashRegisterRepository::class.java).to(PersistentCashRegisterRepository::class.java).`in`(Singleton::class.java) + bind(ReceiptRepository::class.java).to(PersistentReceiptRepository::class.java).`in`(Singleton::class.java) + bind(IdGenerator::class.java).to(SimpleUUIDGenerator::class.java).`in`(Singleton::class.java) } @Provides diff --git a/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentReceiptRepository.kt b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentReceiptRepository.kt new file mode 100644 index 0000000..3ee91e7 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentReceiptRepository.kt @@ -0,0 +1,196 @@ +package com.clouway.pos.print.adapter.db + +import com.clouway.pos.print.core.* +import com.google.inject.Inject +import com.google.inject.Provider +import com.mongodb.client.MongoCollection +import com.mongodb.client.MongoDatabase +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq +import com.mongodb.client.model.Indexes +import com.mongodb.client.model.Updates.set +import org.bson.Document +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.Optional + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class PersistentReceiptRepository @Inject constructor(private val database: Provider, + private val printingListener: PrintingListener, + private val idGenerator: IdGenerator) + : ReceiptRepository { + private val collectionName: String = "receipts" + + /** + * Creates an index on the requestId, isFiscal and receiptId fields. + */ + init { + database.get().getCollection(collectionName) + .createIndex(Indexes.ascending("requestId", "receiptId", "isFiscal")) + } + + override fun register(receiptRequest: PrintReceiptRequest): ReceiptWithStatus { + val receipt = receiptRequest.receipt + + if (receipts() + .find(and( + eq("receipt.receiptId", receipt.receiptId), + eq("isFiscal", receiptRequest.isFiscal))).any()) + throw ReceiptAlreadyRegisteredException() + + val requestId = idGenerator.newId() + + val receiptWithStatus = ReceiptWithStatus( + requestId, + receipt, + receiptRequest.operatorId, + receiptRequest.sourceIp, + receiptRequest.isFiscal, + PrintStatus.PRINTING, + LocalDateTime.now().toInstant(ZoneOffset.UTC).epochSecond + ) + + val receiptWithStatusDoc = receiptWithStatus.toDocument() + + receipts().insertOne(receiptWithStatusDoc) + + return receiptWithStatus + } + + override fun getByRequestId(requestId: String): Optional { + val receiptDoc = receipts().find(and( + eq("requestId", requestId))).firstOrNull() + ?: return Optional.empty() + + return Optional.of(receiptDoc.toReceiptWithStatus()) + } + + override fun getLatest(limit: Int): List { + val receiptList = mutableListOf() + + receipts().find() + .sort(Document("creationSeconds", -1)) + .limit(limit) + .toList() + .forEach { + receiptList.add(it.toReceiptWithStatus()) + } + + return receiptList + } + + override fun getStatus(receiptRequestId: String): PrintStatus { + val receiptDoc = receipts().find(eq("requestId", receiptRequestId)).firstOrNull() + ?: throw ReceiptNotRegisteredException() + + return PrintStatus.valueOf(receiptDoc.getString("printStatus")) + } + + override fun finishPrinting(requestId: String): ReceiptWithStatus { + val receiptDoc = receipts().findOneAndUpdate( + eq("requestId", requestId), + set("printStatus", PrintStatus.PRINTED.name)) + ?: throw ReceiptNotRegisteredException() + + printingListener.onPrinted(receiptDoc.get("receipt", Document::class.java).toReceipt(), PrintStatus.PRINTED) + + return receiptDoc.toReceiptWithStatus() + } + + override fun failPrinting(requestId: String): ReceiptWithStatus { + val receiptDoc = receipts().findOneAndUpdate( + eq("requestId", requestId), + set("printStatus", PrintStatus.FAILED.name)) + ?: throw ReceiptNotRegisteredException() + + printingListener.onPrinted(receiptDoc.get("receipt", Document::class.java).toReceipt(), PrintStatus.FAILED) + + return receiptDoc.toReceiptWithStatus() + } + + private fun receipts(): MongoCollection { + return database.get().getCollection(collectionName) + } + + private fun ReceiptWithStatus.toDocument(): Document { + val receiptDoc = this.receipt.toDocument() + + val receiptWithStatusDoc = Document() + + receiptWithStatusDoc.append("receipt", receiptDoc) + receiptWithStatusDoc["requestId"] = this.requestId + receiptWithStatusDoc["sourceIp"] = this.sourceIp + receiptWithStatusDoc["operatorId"] = this.operatorId + receiptWithStatusDoc["isFiscal"] = this.isFiscal + receiptWithStatusDoc["printStatus"] = this.printStatus.name + receiptWithStatusDoc["creationSecond"] = this.creationSecond + + return receiptWithStatusDoc + } + + + private fun Document.toReceiptWithStatus(): ReceiptWithStatus { + return ReceiptWithStatus( + this.getString("requestId"), + (this["receipt"] as Document).toReceipt(), + this.getString("operatorId"), + this.getString("sourceIp"), + this.getBoolean("isFiscal"), + PrintStatus.valueOf(this.getString("printStatus")), + this.getLong("creationSecond") + ) + } + + @Suppress("UNCHECKED_CAST") + private fun Document.toReceipt(): Receipt { + val receipt = Receipt.Builder() + .withReceiptId(this.getString("receiptId")) + .withAmount(this.getDouble("amount")) + .currency(this.getString("currency")) + .prefixLines(this["prefixLines"] as List) + .suffixLines(this["suffixLines"] as List) + + val itemList = this["receiptItems"] as List + + itemList.forEach { + receipt.addItem(it.toReceiptItem()) + } + + return receipt.build() + } + + private fun Receipt.toDocument(): Document { + val itemList = this.receiptItems + + val docList = mutableListOf() + + itemList.forEach { + docList.add(it.toDocument()) + } + + return Document() + .append("receiptId", this.receiptId) + .append("amount", this.amount) + .append("prefixLines", this.prefixLines()) + .append("suffixLines", this.suffixLines()) + .append("currency", this.currency) + .append("receiptItems", docList) + } + + private fun Document.toReceiptItem(): ReceiptItem { + return ReceiptItem.newItem() + .name(this.getString("name")) + .price(this.getDouble("price")) + .quantity(this.getDouble("quantity")) + .build() + } + + private fun ReceiptItem.toDocument(): Document { + return Document() + .append("name", this.name) + .append("price", this.price) + .append("quantity", this.quantity) + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/adapter/http/HttpModule.java b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/HttpModule.java index f898e2f..be7b3d3 100644 --- a/pos-print/src/main/java/com/clouway/pos/print/adapter/http/HttpModule.java +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/HttpModule.java @@ -17,6 +17,7 @@ protected void configure() { protected void configureSitebricks() { at("/_status").serve(StatusService.class); at("/v1/receipts/req/print").serve(PrintService.class); + at("/v2/receipts").serve(PrintServiceV2.class); at("/v1/devices").serve(DeviceConfigurationService.class); at("/v1/reports").serve(ReportService.class); } diff --git a/pos-print/src/main/java/com/clouway/pos/print/adapter/http/PrintServiceV2.kt b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/PrintServiceV2.kt new file mode 100644 index 0000000..bff8025 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/PrintServiceV2.kt @@ -0,0 +1,94 @@ +package com.clouway.pos.print.adapter.http + +import com.clouway.pos.print.core.PrintQueue +import com.clouway.pos.print.core.PrintReceiptRequest +import com.clouway.pos.print.core.Receipt +import com.clouway.pos.print.core.ReceiptAlreadyRegisteredException +import com.clouway.pos.print.core.ReceiptNotRegisteredException +import com.clouway.pos.print.core.ReceiptRepository +import com.clouway.pos.print.transport.GsonTransport +import com.clouway.pos.print.transport.HtmlTransport +import com.google.inject.name.Named +import com.google.sitebricks.At +import com.google.sitebricks.headless.Reply +import com.google.sitebricks.headless.Request +import com.google.sitebricks.headless.Service +import com.google.sitebricks.http.Get +import com.google.sitebricks.http.Post +import org.slf4j.LoggerFactory +import java.io.File +import javax.inject.Inject + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +@Service +@At("/v2/receipts") +class PrintServiceV2 @Inject constructor(private var repository: ReceiptRepository, + private var queue: PrintQueue) { + private val logger = LoggerFactory.getLogger(PrintServiceV2::class.java) + + @Post + @At("/print") + fun printReceipt(request: Request): Reply<*> { + val dto = request.read(PrintServiceV2.ReceiptDTO::class.java).`as`(GsonTransport::class.java) + + return try { + val receiptRequest = PrintReceiptRequest(dto.receipt, dto.sourceIp, dto.operatorId, dto.fiscal) + + val receiptWithStatus = repository.register(receiptRequest) + + queue.queue(receiptWithStatus) + + logger.info("Receipt with request id ${receiptWithStatus.requestId} queued for printing") + Reply.with(receiptWithStatus.requestId).status(202) + } catch (ex: ReceiptAlreadyRegisteredException) { + logger.error("Receipt with id ${dto.receipt.receiptId} and fiscal status ${dto.fiscal} is already registered in queue") + Reply.saying().badRequest() + } + } + + @Get + @At("/:requestId/status") + fun getReceiptStatus(@Named("requestId") requestId: String): Reply<*> { + return try { + val receiptStatus = repository.getStatus(requestId) + logger.info("Receipt status returned as ${receiptStatus.name}") + Reply.with(receiptStatus).ok() + } catch (ex: ReceiptNotRegisteredException) { + logger.error("Receipt with request id $requestId was not found") + Reply.saying().notFound() + } + } + + @Get + @At("/view") + fun viewReceipts(): Reply<*> { + val htmlPage = File("pos-print/src/main/resources/receipts.html") + + return Reply.with(htmlPage.inputStream()).`as`(HtmlTransport::class.java).ok() + } + + @Get + @At("/latest/:limit") + fun getLatestReceipts(@Named("limit") limit: Int): Reply<*> { + val receipts = repository.getLatest(limit) + + return Reply.with(receipts).`as`(GsonTransport::class.java).ok() + } + + @Post + @At("/:requestId/requeue") + fun requeueReceipt(@Named("requestId") requestId: String): Reply<*> { + val receiptPrintRequest = repository.getByRequestId(requestId) + return if (receiptPrintRequest.isPresent) { + queue.queue(receiptPrintRequest.get()) + Reply.saying().ok() + } else { + logger.error("Receipt with request id $requestId was not found") + Reply.saying().notFound() + } + } + + internal data class ReceiptDTO(val sourceIp: String = "", val operatorId: String = "", val fiscal: Boolean = false, val receipt: Receipt = Receipt.newReceipt().build()) +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintingService.kt b/pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintingService.kt new file mode 100644 index 0000000..813220b --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintingService.kt @@ -0,0 +1,76 @@ +package com.clouway.pos.print.core + +import com.clouway.pos.print.printer.Status +import com.google.common.util.concurrent.AbstractExecutionThreadService +import com.google.inject.Inject +import org.slf4j.LoggerFactory +import java.io.IOException + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class BackgroundReceiptPrintingService @Inject constructor(private var repository: ReceiptRepository, + private var factory: PrinterFactory, + private var queue: PrintQueue) + : AbstractExecutionThreadService() { + + override fun run() { + printReceipts() + } + + private val logger = LoggerFactory.getLogger(BackgroundReceiptPrintingService::class.java) + + fun printReceipts() { + var nextReceiptRequest: ReceiptWithStatus? = queue.next() + + while (nextReceiptRequest != null) { + + val receipt = nextReceiptRequest.receipt + + var printer: ReceiptPrinter? = null + + try { + printer = factory.getPrinter(nextReceiptRequest.sourceIp) + + val printResponse = if (nextReceiptRequest.isFiscal) printer.printFiscalReceipt(receipt) + else printer.printReceipt(receipt) + + if (printResponse.warnings.contains(Status.FISCAL_RECEIPT_IS_OPEN) + || printResponse.warnings.contains(Status.NON_FISCAL_RECEIPT_IS_OPEN)) { + logger.info("Receipt printing accepted") + logger.info(printResponse.warnings.toString()) + repository.finishPrinting(nextReceiptRequest.requestId) + } else { + logger.info("Receipt printing rejected") + logger.info(printResponse.warnings.toString()) + repository.failPrinting(nextReceiptRequest.requestId) + } + + + }catch (ex: ReceiptNotRegisteredException) { + logger.warn("Receipt with request id ${nextReceiptRequest.requestId} was not found in queue") + }catch (ex: DeviceNotFoundException) { + logger.warn("Device was not found") + repository.failPrinting(nextReceiptRequest.requestId) + }catch (ex: IOException){ + logger.warn("Printer threw IO Exception") + repository.failPrinting(nextReceiptRequest.requestId) + }finally { + printer?.close() + nextReceiptRequest = queue.next() + } + } + } + + override fun startUp() { + logger.info("Starting background printing service") + + super.startUp() + } + + override fun triggerShutdown() { + logger.info("Stopping background printing service") + + super.triggerShutdown() + } +} diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/CoreModule.kt b/pos-print/src/main/java/com/clouway/pos/print/core/CoreModule.kt index 5b83942..8e344e3 100644 --- a/pos-print/src/main/java/com/clouway/pos/print/core/CoreModule.kt +++ b/pos-print/src/main/java/com/clouway/pos/print/core/CoreModule.kt @@ -2,6 +2,7 @@ package com.clouway.pos.print.core import com.clouway.pos.print.printer.FP705PrinterFactory import com.google.inject.AbstractModule +import com.google.inject.Singleton /** * @author Martin Milev @@ -10,5 +11,7 @@ class CoreModule : AbstractModule() { override fun configure() { bind(PrinterFactory::class.java).to(FP705PrinterFactory::class.java) + bind(PrintQueue::class.java).to(InMemoryPrintQueue::class.java).`in`(Singleton::class.java) + bind(PrintingListener::class.java).to(ReceiptPrintingListener::class.java).`in`(Singleton::class.java) } } \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/IdGenerator.kt b/pos-print/src/main/java/com/clouway/pos/print/core/IdGenerator.kt new file mode 100644 index 0000000..cca8923 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/IdGenerator.kt @@ -0,0 +1,10 @@ +package com.clouway.pos.print.core + +/** + * Provides the methods to generate unique ids. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface IdGenerator { + fun newId() : String +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt b/pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt new file mode 100644 index 0000000..e842f02 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt @@ -0,0 +1,21 @@ +package com.clouway.pos.print.core + +import java.util.concurrent.ArrayBlockingQueue + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class InMemoryPrintQueue : PrintQueue { + + private val queueMax = 50 + + private val queue = ArrayBlockingQueue(queueMax) + + override fun next(): ReceiptWithStatus { + return queue.take() + } + + override fun queue(receiptWithStatus: ReceiptWithStatus) { + queue.offer(receiptWithStatus) + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/PrintQueue.kt b/pos-print/src/main/java/com/clouway/pos/print/core/PrintQueue.kt new file mode 100644 index 0000000..82d1f6c --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/PrintQueue.kt @@ -0,0 +1,22 @@ +package com.clouway.pos.print.core + +/** + * Provides the methods to iterate over and append to a + * queue of receipts. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface PrintQueue { + + /** + * Returns the next receipt. + */ + fun next(): ReceiptWithStatus? + + /** + * Queues a receipt along with its status. + * + * @param receiptWithStatus the receipt to queue + */ + fun queue(receiptWithStatus: ReceiptWithStatus) +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/PrintReceiptRequest.kt b/pos-print/src/main/java/com/clouway/pos/print/core/PrintReceiptRequest.kt new file mode 100644 index 0000000..92d1bbe --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/PrintReceiptRequest.kt @@ -0,0 +1,12 @@ +package com.clouway.pos.print.core + +/** + * An object containing a receipt and information on who + * sent the print request. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +data class PrintReceiptRequest(val receipt: Receipt, + val sourceIp: String, + val operatorId: String, + val isFiscal: Boolean) \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/PrintingListener.kt b/pos-print/src/main/java/com/clouway/pos/print/core/PrintingListener.kt new file mode 100644 index 0000000..cabfd2f --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/PrintingListener.kt @@ -0,0 +1,17 @@ +package com.clouway.pos.print.core + +/** + * Provides the methods to give feedback after a print + * has been attempted. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface PrintingListener { + /** + * Notifies when a printing status has been updated. + * + * @param receipt the updated receipt + * @param printStatus the new status + */ + fun onPrinted(receipt: Receipt, printStatus: PrintStatus) +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintingListener.kt b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintingListener.kt new file mode 100644 index 0000000..040c4a3 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintingListener.kt @@ -0,0 +1,16 @@ +package com.clouway.pos.print.core + +import org.slf4j.LoggerFactory + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class ReceiptPrintingListener : PrintingListener { + private val logger = LoggerFactory.getLogger(PrintingListener::class.java) + + override fun onPrinted(receipt: Receipt, printStatus: PrintStatus) { + logger.info("Receipt was processed") + logger.info(receipt.receiptId) + logger.info(printStatus.name) + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptRepository.kt b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptRepository.kt new file mode 100644 index 0000000..e651e81 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptRepository.kt @@ -0,0 +1,73 @@ +package com.clouway.pos.print.core + +import java.util.Optional + +/** + * Provides the methods to persist, save and check the status + * of requested receipts. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface ReceiptRepository { + + /** + * Registers a receipt request. + * + * @param receiptRequest the receipt to register + * @return the persisted receipt with status + */ + @Throws(ReceiptAlreadyRegisteredException::class) + fun register(receiptRequest: PrintReceiptRequest): ReceiptWithStatus + + /** + * Returns a receipt and its status and sender information + * by a given request id. + * + * @param requestId the id of the request to return. + * @return a ReceiptWithStatus + */ + fun getByRequestId(requestId: String): Optional + + /** + * Returns the printing status of a receipt. + * + * @param receiptRequestId the id of the receipt request + * @return the status of the receipt + */ + @Throws(ReceiptNotRegisteredException::class) + fun getStatus(receiptRequestId: String): PrintStatus + + /** + * Returns the latest receipts up to a limit. + * + * @param limit The amount of receipts to return. + * @return A map of receipts with their request ids as keys. + */ + fun getLatest(limit: Int): List + + /** + * Marks a receipt as Printed. + * + * @param requestId the id of the receipt to finish + * @return the finished receipt + */ + @Throws(ReceiptNotRegisteredException::class) + fun finishPrinting(requestId: String): ReceiptWithStatus + + /** + * Marks a receipt as Failed. + * + * @param requestId the id of the receipt to fail. + * @return the rejected receipt + */ + @Throws(ReceiptNotRegisteredException::class) + fun failPrinting(requestId: String): ReceiptWithStatus +} + + +internal class ReceiptNotRegisteredException : Throwable() +internal class ReceiptAlreadyRegisteredException : Throwable() + +enum class PrintStatus { + PRINTING, PRINTED, FAILED +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptWithStatus.kt b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptWithStatus.kt new file mode 100644 index 0000000..7e0f6c6 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptWithStatus.kt @@ -0,0 +1,15 @@ +package com.clouway.pos.print.core + +/** + * An object wrapping a receipt with the information + * of the sender and the reference to the persisted document. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +data class ReceiptWithStatus (val requestId: String, + val receipt: Receipt, + val operatorId: String, + val sourceIp: String, + val isFiscal: Boolean, + val printStatus: PrintStatus, + val creationSecond: Long) \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/SimpleUUIDGenerator.kt b/pos-print/src/main/java/com/clouway/pos/print/core/SimpleUUIDGenerator.kt new file mode 100644 index 0000000..e72840a --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/SimpleUUIDGenerator.kt @@ -0,0 +1,12 @@ +package com.clouway.pos.print.core + +import java.util.UUID + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class SimpleUUIDGenerator : IdGenerator { + override fun newId(): String { + return UUID.randomUUID().toString() + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/transport/HtmlTransport.java b/pos-print/src/main/java/com/clouway/pos/print/transport/HtmlTransport.java new file mode 100644 index 0000000..1bbbe58 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/transport/HtmlTransport.java @@ -0,0 +1,34 @@ +package com.clouway.pos.print.transport; + +import com.google.inject.TypeLiteral; +import com.google.sitebricks.client.Transport; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.apache.commons.io.IOUtils; + +public class HtmlTransport implements Transport { + + @Override + public T in(InputStream inputStream, Class tClass) throws IOException { + String content = IOUtils.toString(inputStream); + return (T) content; + } + + @Override + public T in(InputStream inputStream, TypeLiteral typeLiteral) throws IOException { + String content = IOUtils.toString(inputStream); + return (T) content; + } + + @Override + public void out(OutputStream outputStream, Class tClass, T t) throws IOException { + String content = (String) t; + IOUtils.write(content, outputStream, "UTF-8"); + } + + @Override + public String contentType() { + return "text/html"; + } +} \ No newline at end of file diff --git a/pos-print/src/main/resources/receipts.html b/pos-print/src/main/resources/receipts.html new file mode 100644 index 0000000..f87a33d --- /dev/null +++ b/pos-print/src/main/resources/receipts.html @@ -0,0 +1,100 @@ + + + + Admin Page + + + + + + + + + +
+
+
+
Receipts List
+
+ +
{{ message }}
+ + + + + + + + + + + + + + + + + + +
statusidamountfiscal
{{receipt.printStatus}}{{receipt.requestId}}{{receipt.receipt.amount}}{{receipt.isFiscal}} + +
+ +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/pos-print/src/test/java/com/clouway/pos/print/adapter/http/PrintServiceV2Test.kt b/pos-print/src/test/java/com/clouway/pos/print/adapter/http/PrintServiceV2Test.kt new file mode 100644 index 0000000..e3fcd22 --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/adapter/http/PrintServiceV2Test.kt @@ -0,0 +1,144 @@ +package com.clouway.pos.print.adapter.http + +import com.clouway.pos.print.FakeRequest +import com.clouway.pos.print.ReplyMatchers +import com.clouway.pos.print.core.PrintQueue +import com.clouway.pos.print.core.PrintReceiptRequest +import com.clouway.pos.print.core.PrintStatus +import com.clouway.pos.print.core.Receipt +import com.clouway.pos.print.core.ReceiptAlreadyRegisteredException +import com.clouway.pos.print.core.ReceiptNotRegisteredException +import com.clouway.pos.print.core.ReceiptRepository +import com.clouway.pos.print.core.ReceiptWithStatus +import org.hamcrest.MatcherAssert +import org.jmock.AbstractExpectations.returnValue +import org.jmock.AbstractExpectations.throwException +import org.jmock.Expectations +import org.jmock.Mockery +import org.jmock.integration.junit4.JUnitRuleMockery +import org.junit.Rule +import org.junit.Test +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.Optional + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class PrintServiceV2Test { + + @Rule + @JvmField + var context = JUnitRuleMockery() + + private fun Mockery.expecting(block: Expectations.() -> Unit) { + checking(Expectations().apply(block)) + } + + private val repo = context.mock(ReceiptRepository::class.java) + private val queue = context.mock(PrintQueue::class.java) + + private val receipt = Receipt.newReceipt().withReceiptId("::receipt-id::").build() + + private val sourceIp = "::sourceIp::" + private val operatorId = "::operatorId::" + + private val fiscalReceiptDTO = PrintServiceV2 + .ReceiptDTO(sourceIp, operatorId, true, receipt = receipt) + private val nonFiscalReceiptDTO = PrintServiceV2 + .ReceiptDTO(sourceIp, operatorId, false, receipt = receipt) + + private val fiscalRequestId = "::fiscal-request-id::" + private val nonFiscalRequestId = "::non-fiscal-request-id::" + private val instant = LocalDateTime.now().toInstant(ZoneOffset.UTC).epochSecond + + private val fiscalReceiptRequest = PrintReceiptRequest(receipt, sourceIp, operatorId, true) + private val fiscalReceiptWithStatus = ReceiptWithStatus(fiscalRequestId, receipt, operatorId, sourceIp, true, PrintStatus.PRINTING, instant) + + private val nonFiscalReceiptRequest = PrintReceiptRequest(receipt, sourceIp, operatorId, false) + private val nonFiscalReceiptWithStatus = ReceiptWithStatus(nonFiscalRequestId, receipt, operatorId, sourceIp, false, PrintStatus.PRINTING, instant) + + private val service = PrintServiceV2(repo, queue) + + @Test + fun registerReceiptsForPrinting() { + context.expecting { + oneOf(repo).register(fiscalReceiptRequest) + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(repo).register(nonFiscalReceiptRequest) + will(returnValue(nonFiscalReceiptWithStatus)) + + oneOf(queue).queue(fiscalReceiptWithStatus) + oneOf(queue).queue(nonFiscalReceiptWithStatus) + } + + val fiscalReply = service.printReceipt(FakeRequest.newRequest(fiscalReceiptDTO)) + val nonFiscalReply = service.printReceipt(FakeRequest.newRequest(nonFiscalReceiptDTO)) + + MatcherAssert.assertThat(fiscalReply, ReplyMatchers.isAccepted) + MatcherAssert.assertThat(fiscalReply, ReplyMatchers.contains(fiscalRequestId)) + MatcherAssert.assertThat(nonFiscalReply, ReplyMatchers.isAccepted) + MatcherAssert.assertThat(nonFiscalReply, ReplyMatchers.contains(nonFiscalRequestId)) + } + + @Test + fun savingAlreadyExistingReceiptThrowsException() { + context.expecting { + oneOf(repo).register(fiscalReceiptRequest) + will(throwException(ReceiptAlreadyRegisteredException())) + } + + val reply = service.printReceipt(FakeRequest.newRequest(fiscalReceiptDTO)) + MatcherAssert.assertThat(reply, ReplyMatchers.isBadRequest) + } + + @Test + fun getReceiptStatus() { + context.expecting { + oneOf(repo).getStatus(receipt.receiptId) + will(returnValue(PrintStatus.PRINTING)) + } + + val reply = service.getReceiptStatus(receipt.receiptId) + MatcherAssert.assertThat(reply, ReplyMatchers.isOk) + MatcherAssert.assertThat(reply, ReplyMatchers.contains(PrintStatus.PRINTING)) + } + + @Test + fun getLatestReceipts() { + context.expecting { + oneOf(repo).getLatest(1) + will(returnValue(listOf(fiscalReceiptWithStatus))) + } + + val reply = service.getLatestReceipts(1) + MatcherAssert.assertThat(reply, ReplyMatchers.isOk) + MatcherAssert.assertThat(reply, ReplyMatchers + .contains(listOf(fiscalReceiptWithStatus))) + } + + @Test + fun requeueReceiptByRequestId(){ + context.expecting { + oneOf(repo).getByRequestId("::request-id::") + will(returnValue(Optional.of(fiscalReceiptWithStatus))) + + oneOf(queue).queue(fiscalReceiptWithStatus) + } + + val reply = service.requeueReceipt("::request-id::") + MatcherAssert.assertThat(reply, ReplyMatchers.isOk) + } + + @Test + fun gettingReceiptStatusOfNonExistingReturnsNotFound() { + context.expecting { + oneOf(repo).getStatus(receipt.receiptId) + will(throwException(ReceiptNotRegisteredException())) + } + + val reply = service.getReceiptStatus(receipt.receiptId) + MatcherAssert.assertThat(reply, ReplyMatchers.isNotFound) + } +} \ No newline at end of file diff --git a/pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintingServiceTest.kt b/pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintingServiceTest.kt new file mode 100644 index 0000000..758d288 --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintingServiceTest.kt @@ -0,0 +1,152 @@ +package com.clouway.pos.print.core + +import com.clouway.pos.print.persistent.DatastoreCleaner +import com.clouway.pos.print.persistent.DatastoreRule +import com.clouway.pos.print.printer.Status +import org.jmock.AbstractExpectations.returnValue +import org.jmock.AbstractExpectations.throwException +import org.jmock.Expectations +import org.jmock.Mockery +import org.jmock.integration.junit4.JUnitRuleMockery +import org.jmock.lib.concurrent.Synchroniser +import org.junit.Rule +import org.junit.Test +import java.io.IOException +import java.time.LocalDateTime +import java.time.ZoneOffset + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class BackgroundReceiptPrintingServiceTest { + + @Rule + @JvmField + var context = JUnitRuleMockery() + + init { + context.setThreadingPolicy(Synchroniser()) + } + + @Rule + @JvmField + val dataStoreRule = DatastoreRule() + + @Rule + @JvmField + var cleaner = DatastoreCleaner(dataStoreRule.db()) + + private val repo = context.mock(ReceiptRepository::class.java) + private val factory = context.mock(PrinterFactory::class.java) + private val queue = context.mock(PrintQueue::class.java) + + private val service = BackgroundReceiptPrintingService(repo, factory, queue) + + private val printer = context.mock(ReceiptPrinter::class.java) + + private val receipt = Receipt.newReceipt() + .withReceiptId("::receiptId::") + .withAmount(200.0) + .build() + + private val requestId = "::requestId::" + private val sourceIp = "::sourceIp::" + private val operatorId = "::operatorId::" + private val isFiscal = true + private val instant = LocalDateTime.now().toInstant(ZoneOffset.UTC).epochSecond + + private val fiscalReceiptWithStatus = ReceiptWithStatus(requestId, receipt, operatorId, sourceIp, isFiscal, PrintStatus.PRINTING, instant) + + private val acceptedPrintingResponse = PrintReceiptResponse(setOf(Status.FISCAL_RECEIPT_IS_OPEN)) + private val rejectedPrintingResponse = PrintReceiptResponse(setOf(Status.BROKEN_PRINTIN_MECHANISM)) + + @Test + fun printReceipt() { + context.expecting { + oneOf(queue).next() + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(factory).getPrinter(sourceIp) + will(returnValue(printer)) + + oneOf(printer).printFiscalReceipt(receipt) + will(returnValue(acceptedPrintingResponse)) + + oneOf(repo).finishPrinting(requestId) + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(printer).close() + + oneOf(queue).next() + will(returnValue(null)) + } + + service.printReceipts() + } + + @Test + fun failPrintingReceiptWhenNotPrinted() { + context.expecting { + oneOf(queue).next() + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(factory).getPrinter(sourceIp) + will(returnValue(printer)) + + oneOf(printer).printFiscalReceipt(receipt) + will(returnValue(rejectedPrintingResponse)) + + oneOf(repo).failPrinting(requestId) + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(printer).close() + + oneOf(queue).next() + will(returnValue(null)) + } + + service.printReceipts() + } + + @Test + fun failPrintingWhenDeviceNotFound() { + context.expecting { + oneOf(queue).next() + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(factory).getPrinter(sourceIp) + will(throwException(DeviceNotFoundException())) + + oneOf(repo).failPrinting(requestId) + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(queue).next() + will(returnValue(null)) + } + + service.printReceipts() + } + + @Test + fun failPrintingWithIOException() { + context.expecting { + oneOf(queue).next() + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(factory).getPrinter(sourceIp) + will(throwException(IOException())) + + oneOf(repo).failPrinting(requestId) + will(returnValue(fiscalReceiptWithStatus)) + + oneOf(queue).next() + will(returnValue(null)) + } + + service.printReceipts() + } + + private fun Mockery.expecting(block: Expectations.() -> Unit) { + checking(Expectations().apply(block)) + } +} \ No newline at end of file diff --git a/pos-print/src/test/java/com/clouway/pos/print/persistent/PersistentReceiptRepositoryTest.kt b/pos-print/src/test/java/com/clouway/pos/print/persistent/PersistentReceiptRepositoryTest.kt new file mode 100644 index 0000000..f4a7c3c --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/persistent/PersistentReceiptRepositoryTest.kt @@ -0,0 +1,182 @@ +package com.clouway.pos.print.persistent + +import com.clouway.pos.print.adapter.db.PersistentReceiptRepository +import com.clouway.pos.print.core.IdGenerator +import com.clouway.pos.print.core.PrintReceiptRequest +import com.clouway.pos.print.core.PrintStatus +import com.clouway.pos.print.core.PrintingListener +import com.clouway.pos.print.core.Receipt +import com.clouway.pos.print.core.ReceiptAlreadyRegisteredException +import com.clouway.pos.print.core.ReceiptNotRegisteredException +import com.google.inject.util.Providers +import org.jmock.AbstractExpectations.returnValue +import org.jmock.Expectations +import org.jmock.Mockery +import org.jmock.integration.junit4.JUnitRuleMockery +import org.junit.Assert.assertThat +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import org.hamcrest.CoreMatchers.`is` as Is + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class PersistentReceiptRepositoryTest { + companion object { + @ClassRule + @JvmField + val dataStoreRule = DatastoreRule() + } + + @Rule + @JvmField + val context: JUnitRuleMockery = JUnitRuleMockery() + + @Rule + @JvmField + var cleaner = DatastoreCleaner(dataStoreRule.db()) + + private val idGenerator = context.mock(IdGenerator::class.java) + + private val printingListener = context.mock(PrintingListener::class.java) + + private val repository = PersistentReceiptRepository(Providers.of(dataStoreRule.db()), + printingListener, idGenerator) + + private val receiptId = "::receiptId::" + private val requestId = "::requestId::" + + private val receipt = Receipt.newReceipt().withReceiptId(receiptId).build() + + private val sourceIp = "::sourceIp::" + private val operatorId = "::operatorId::" + + private val fiscalReceiptRequest = PrintReceiptRequest(receipt, sourceIp, operatorId, true) + private val nonFiscalReceiptRequest = PrintReceiptRequest(receipt, sourceIp, operatorId, false) + + @Test + fun happyPath() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + } + + val fiscalReceiptWithStatus = repository.register(fiscalReceiptRequest) + val nonFiscalReceiptWithStatus = repository.register(nonFiscalReceiptRequest) + + assertThat(repository.getStatus(fiscalReceiptWithStatus.requestId), Is(PrintStatus.PRINTING)) + assertThat(repository.getStatus(nonFiscalReceiptWithStatus.requestId), Is(PrintStatus.PRINTING)) + } + + @Test(expected = ReceiptAlreadyRegisteredException::class) + fun registeringSameReceiptThrowsException() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + } + + repository.register(fiscalReceiptRequest) + repository.register(fiscalReceiptRequest) + } + + @Test + fun getReceiptById() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + } + + val receiptWithStatus = repository.register(fiscalReceiptRequest) + + val retrievedReceipt = repository.getByRequestId(receiptWithStatus.requestId) + + assertThat(retrievedReceipt.isPresent, Is(true)) + assertThat(retrievedReceipt.get(), Is(receiptWithStatus)) + } + + @Test + fun getReceiptByIdReturnsEmptyWhenNotFound() { + val retrievedReceipt = repository.getByRequestId("::non-existent-receipt::") + + assertThat(retrievedReceipt.isPresent, Is(false)) + } + + @Test + fun getLastReceipts() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + } + + val nonFiscalReceiptWithStatus = repository.register(nonFiscalReceiptRequest) + val fiscalReceiptWithStatus = repository.register(fiscalReceiptRequest) + + val lastReceipts = repository.getLatest(2) + + assertThat(lastReceipts, Is(listOf(nonFiscalReceiptWithStatus, fiscalReceiptWithStatus))) + } + + @Test + fun getReceiptStatus() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + } + + val receiptWithStatus = repository.register(fiscalReceiptRequest) + + assertThat(repository.getStatus(receiptWithStatus.requestId), Is(receiptWithStatus.printStatus)) + } + + @Test(expected = ReceiptNotRegisteredException::class) + fun gettingStatusOfNonExistingReceiptThrowsException() { + repository.getStatus("::non-existent-receiptId::") + } + + @Test + fun finishPrintingReceipt() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + + oneOf(printingListener).onPrinted(receipt, PrintStatus.PRINTED) + } + + val receiptWithStatus = repository.register(fiscalReceiptRequest) + val finishedReceipt = repository.finishPrinting(receiptWithStatus.requestId) + + assertThat(repository.getStatus(receiptWithStatus.requestId), Is(PrintStatus.PRINTED)) + assertThat(finishedReceipt, Is(receiptWithStatus)) + } + + @Test(expected = ReceiptNotRegisteredException::class) + fun finishingNonExistingReceiptThrowsException() { + repository.finishPrinting("::fake-receiptId::") + } + + @Test + fun rejectPrintingReceipt() { + context.expecting { + allowing(idGenerator).newId() + will(returnValue(requestId)) + + oneOf(printingListener).onPrinted(receipt, PrintStatus.FAILED) + } + + val receiptWithStatus = repository.register(fiscalReceiptRequest) + val failedReceipt = repository.failPrinting(receiptWithStatus.requestId) + + assertThat(repository.getStatus(receiptWithStatus.requestId), Is(PrintStatus.FAILED)) + assertThat(failedReceipt, Is(receiptWithStatus)) + } + + @Test(expected = ReceiptNotRegisteredException::class) + fun rejectingNonExistingReceiptThrowsException() { + repository.finishPrinting("::fake-receiptId::") + } + + private fun Mockery.expecting(block: Expectations.() -> Unit) { + checking(Expectations().apply(block)) + } +} \ No newline at end of file