From 82669690102d365832319c09a6e26e051d0c991d Mon Sep 17 00:00:00 2001 From: Tsvetozar Bonev Date: Tue, 18 Sep 2018 14:40:52 +0300 Subject: [PATCH] pos-print: add async receipt printing --- pos-print/build.gradle | 9 +- .../clouway/pos/print/PosPrintService.java | 5 + .../pos/print/adapter/db/PersistentModule.kt | 2 + .../adapter/db/PersistentReceiptRepository.kt | 80 +++++++++ .../adapter/db/ReceiptConverterHelpers.kt | 65 +++++++ .../print/adapter/http/AsyncPrintService.kt | 54 ++++++ .../pos/print/adapter/http/HttpModule.java | 1 + .../core/BackgroundReceiptPrintWorker.kt | 78 ++++++++ .../pos/print/core/ConsolePrintNotifier.kt | 11 ++ .../com/clouway/pos/print/core/CoreModule.kt | 4 + .../pos/print/core/InMemoryPrintQueue.kt | 29 +++ .../clouway/pos/print/core/PrintNotifier.kt | 16 ++ .../com/clouway/pos/print/core/PrintQueue.kt | 31 ++++ .../pos/print/core/ReceiptPrintWorker.kt | 20 +++ .../pos/print/core/ReceiptRepository.kt | 71 ++++++++ .../adapter/http/AsyncPrintServiceTest.kt | 90 ++++++++++ .../core/BackgroundReceiptPrintWorkerTest.kt | 168 ++++++++++++++++++ .../PersistentReceiptRepositoryTest.kt | 111 ++++++++++++ 18 files changed, 842 insertions(+), 3 deletions(-) create mode 100644 pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentReceiptRepository.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/adapter/db/ReceiptConverterHelpers.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/adapter/http/AsyncPrintService.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorker.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/ConsolePrintNotifier.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/PrintNotifier.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/PrintQueue.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintWorker.kt create mode 100644 pos-print/src/main/java/com/clouway/pos/print/core/ReceiptRepository.kt create mode 100644 pos-print/src/test/java/com/clouway/pos/print/adapter/http/AsyncPrintServiceTest.kt create mode 100644 pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorkerTest.kt create mode 100644 pos-print/src/test/java/com/clouway/pos/print/persistent/PersistentReceiptRepositoryTest.kt diff --git a/pos-print/build.gradle b/pos-print/build.gradle index b2f8ec6..e570ba3 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.61' repositories { mavenCentral() jcenter() @@ -83,11 +83,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..e5b778f 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.ReceiptPrintWorker; import com.clouway.pos.print.adapter.http.HttpBackend; import com.clouway.pos.print.adapter.http.HttpModule; import com.clouway.pos.print.core.CoreModule; @@ -39,9 +40,13 @@ public static void main(String[] args) { new PersistentModule(client, commandCLI.dbName()) ); + HttpBackend backend = new HttpBackend(commandCLI.httpPort(), injector); backend.start(); + ReceiptPrintWorker backgroundPrinter = injector.getInstance(ReceiptPrintWorker.class); + backgroundPrinter.start(); + System.out.printf("POS Print Service is up and running on port: %d\n", commandCLI.httpPort()); Runtime.getRuntime().addShutdownHook(new Thread(() -> { 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..015e0b2 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,6 @@ package com.clouway.pos.print.adapter.db +import com.clouway.pos.print.core.ReceiptRepository import com.google.inject.AbstractModule import com.google.inject.Provides import com.google.inject.Singleton @@ -13,6 +14,7 @@ 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) } @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..4d0a0a8 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/PersistentReceiptRepository.kt @@ -0,0 +1,80 @@ +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.eq +import com.mongodb.client.model.Updates.set +import org.bson.Document + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class PersistentReceiptRepository @Inject constructor(private val database: Provider) + : ReceiptRepository { + + private val collectionName: String = "receipts" + + override fun save(receipt: Receipt): String { + if (receipts().find(eq("receiptId", receipt.receiptId)).firstOrNull() != null) + throw ReceiptAlreadyInQueueException() + + val receiptDoc = receipt.toDocument() + + receipts().insertOne(receiptDoc) + + return receipt.receiptId + } + + override fun getStatus(receiptId: String): ReceiptStatus { + val receiptDoc = receipts().find( + eq("receiptId", receiptId)).firstOrNull() + ?: throw ReceiptNotInQueueException() + + return ReceiptStatus.valueOf(receiptDoc.getString("status")) + } + + override fun remove(receiptId: String): Receipt { + val receiptDoc = receipts().findOneAndDelete( + eq("receiptId", receiptId)) + ?: throw ReceiptNotInQueueException() + + return receiptDoc.toReceipt() + } + + override fun getByStatus(receiptStatus: ReceiptStatus): List { + val receiptDocs = receipts().find(eq("status", receiptStatus.name)) + + val receiptList = mutableListOf() + + receiptDocs.forEach { + receiptList.add(it.toReceipt()) + } + + return receiptList + } + + override fun finishPrinting(receiptId: String): Receipt { + val receiptDoc = receipts().findOneAndUpdate( + eq("receiptId", receiptId), + set("status", ReceiptStatus.PRINTED.name)) + ?: throw ReceiptNotInQueueException() + + return receiptDoc.toReceipt() + } + + override fun failPrinting(receiptId: String): Receipt { + val receiptDoc = receipts().findOneAndUpdate( + eq("receiptId", receiptId), + set("status", ReceiptStatus.FAILED.name)) + ?: throw ReceiptNotInQueueException() + + return receiptDoc.toReceipt() + } + + private fun receipts(): MongoCollection { + return database.get().getCollection(collectionName) + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/adapter/db/ReceiptConverterHelpers.kt b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/ReceiptConverterHelpers.kt new file mode 100644 index 0000000..190f24d --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/db/ReceiptConverterHelpers.kt @@ -0,0 +1,65 @@ +package com.clouway.pos.print.adapter.db + +import com.clouway.pos.print.core.Receipt +import com.clouway.pos.print.core.ReceiptItem +import com.clouway.pos.print.core.ReceiptStatus +import org.bson.Document + +/** + * Helpers providing conversion between mongo bson docs and + * receipt domain objects. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ + +@Suppress("UNCHECKED_CAST") +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() +} + +fun Document.toReceiptItem(): ReceiptItem{ + return ReceiptItem.newItem() + .name(this.getString("name")) + .price(this.getDouble("price")) + .quantity(this.getDouble("quantity")) + .build() +} + +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) + .append("status", ReceiptStatus.PRINTING.name) +} + +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/AsyncPrintService.kt b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/AsyncPrintService.kt new file mode 100644 index 0000000..c55f38b --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/AsyncPrintService.kt @@ -0,0 +1,54 @@ +package com.clouway.pos.print.adapter.http + +import com.clouway.pos.print.core.* +import com.clouway.pos.print.transport.GsonTransport +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 javax.inject.Inject + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +@Service +@At("/v2/receipts/req/print") +class AsyncPrintService @Inject constructor(private var repository: ReceiptRepository, + private var queue: PrintQueue) { + private val logger = LoggerFactory.getLogger(AsyncPrintService::class.java) + + @Post + fun printReceipt(request: Request): Reply<*> { + return try { + val dto = request.read(ReceiptDTO::class.java).`as`(GsonTransport::class.java) + val receiptId = repository.save(dto.receipt) + + queue.queue(PrintReceiptRequest(dto.receipt, dto.sourceIp, dto.fiscal)) + + logger.info("Receipt queued for printing") + Reply.with(receiptId).ok() + } catch (ex: ReceiptAlreadyInQueueException) { + logger.error("Receipt is already in queue") + Reply.saying().badRequest() + } + } + + @Get + @At("/status/:id") + fun getReceiptStatus(@Named("id") id: String): Reply<*> { + return try { + val receiptStatus = repository.getStatus(id) + logger.info("Receipt status returned as ${receiptStatus.name}") + Reply.with(receiptStatus).ok() + }catch (ex: ReceiptNotInQueueException){ + logger.error("Receipt 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/adapter/http/HttpModule.java b/pos-print/src/main/java/com/clouway/pos/print/adapter/http/HttpModule.java index f898e2f..a5edc63 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/req/print").serve(AsyncPrintService.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/core/BackgroundReceiptPrintWorker.kt b/pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorker.kt new file mode 100644 index 0000000..d417243 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorker.kt @@ -0,0 +1,78 @@ +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 BackgroundReceiptPrintWorker @Inject constructor(private var repository: ReceiptRepository, + private var factory: PrinterFactory, + private var notifier: PrintNotifier, + private var queue: PrintQueue) + : ReceiptPrintWorker, AbstractExecutionThreadService() { + + override fun run() { + while (isRunning){ + printReceipts() + } + } + + private val logger = LoggerFactory.getLogger(ReceiptPrintWorker::class.java) + + override fun shutDown() { + logger.info("Stopping background printing service") + + super.shutDown() + } + override fun printReceipts() { + while (queue.hasNext()) { + val receiptWithSource = queue.next() + println(receiptWithSource) + val receipt = receiptWithSource.receipt + + var printer: ReceiptPrinter? = null + + try { + printer = factory.getPrinter(receiptWithSource.sourceIp) + + val printResponse = if (receiptWithSource.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") + repository.finishPrinting(receipt.receiptId) + } else { + logger.info("Receipt printing rejected") + repository.failPrinting(receipt.receiptId) + } + + notifier.notifyPrinted(printResponse) + + }catch (ex: ReceiptNotInQueueException) { + logger.warn("Receipt was not found in queue") + }catch (ex: DeviceNotFoundException) { + logger.warn("Device was not found") + repository.failPrinting(receipt.receiptId) + println("Receipt is ${receipt.receiptId}") + notifier.notifyPrinted(PrintReceiptResponse(setOf(Status.GENERAL_ERROR))) + }catch (ex: IOException){ + logger.warn("Printer threw IO Exception") + repository.failPrinting(receipt.receiptId) + notifier.notifyPrinted(PrintReceiptResponse(setOf(Status.GENERAL_ERROR))) + }finally { + printer?.close() + } + } + } + + override fun start() { + logger.info("Starting background printing service") + + this.startAsync() + } +} diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/ConsolePrintNotifier.kt b/pos-print/src/main/java/com/clouway/pos/print/core/ConsolePrintNotifier.kt new file mode 100644 index 0000000..16d25e5 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ConsolePrintNotifier.kt @@ -0,0 +1,11 @@ +package com.clouway.pos.print.core + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class ConsolePrintNotifier : PrintNotifier { + override fun notifyPrinted(printResponse: PrintReceiptResponse) { + Thread.sleep(1000) + println(printResponse) + } +} \ No newline at end of file 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..c8e449c 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,8 @@ class CoreModule : AbstractModule() { override fun configure() { bind(PrinterFactory::class.java).to(FP705PrinterFactory::class.java) + bind(PrintNotifier::class.java).to(ConsolePrintNotifier::class.java) + bind(PrintQueue::class.java).to(InMemoryPrintQueue::class.java).`in`(Singleton::class.java) + bind(ReceiptPrintWorker::class.java).to(BackgroundReceiptPrintWorker::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/InMemoryPrintQueue.kt b/pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt new file mode 100644 index 0000000..f34aed6 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/InMemoryPrintQueue.kt @@ -0,0 +1,29 @@ +package com.clouway.pos.print.core + +import org.eclipse.jetty.util.BlockingArrayQueue + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class InMemoryPrintQueue : PrintQueue { + + private val queue = BlockingArrayQueue() + + /** + * Block until queue has a next element. + */ + override fun hasNext(): Boolean { + while(true){ + if(queue.peek() != null) return true + else Thread.sleep(100) + } + } + + override fun next(): PrintReceiptRequest { + return queue.poll() + } + + override fun queue(printReceiptRequest: PrintReceiptRequest) { + queue.offer(printReceiptRequest) + } +} \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/PrintNotifier.kt b/pos-print/src/main/java/com/clouway/pos/print/core/PrintNotifier.kt new file mode 100644 index 0000000..565b036 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/PrintNotifier.kt @@ -0,0 +1,16 @@ +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 PrintNotifier { + /** + * Notifies that a print response has been created. + * + * @param printResponse the response to send + */ + fun notifyPrinted(printResponse: PrintReceiptResponse) +} \ 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..d91c4a5 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/PrintQueue.kt @@ -0,0 +1,31 @@ +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 { + + /** + * Checks if there is a next receipt, blocks until a next + * one is found. + */ + fun hasNext(): Boolean + + /** + * Returns the next receipt. + */ + fun next(): PrintReceiptRequest + + /** + * Queues a receipt along with its source and + * fiscal state to the queue. + * + * @param printReceiptRequest the receipt to queue + */ + fun queue(printReceiptRequest: PrintReceiptRequest) +} + +data class PrintReceiptRequest(val receipt: Receipt, val sourceIp: String, val isFiscal: Boolean) \ No newline at end of file diff --git a/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintWorker.kt b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintWorker.kt new file mode 100644 index 0000000..c29373d --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptPrintWorker.kt @@ -0,0 +1,20 @@ +package com.clouway.pos.print.core + +/** + * Provides the methods to print a collection of receipts + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface ReceiptPrintWorker { + + /** + * Starts the worker in the background and + * blocks until it is running. + */ + fun start() + + /** + * Prints the receipts it has stored. + */ + fun printReceipts() +} \ 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..5421171 --- /dev/null +++ b/pos-print/src/main/java/com/clouway/pos/print/core/ReceiptRepository.kt @@ -0,0 +1,71 @@ +package com.clouway.pos.print.core + +/** + * Provides the methods to persist, save and check the status + * of requested receipts. + * + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +interface ReceiptRepository { + + /** + * Saves a receipt in persistence. + * + * @param receipt the receipt to save + * @return the id of the receipt + */ + @Throws(ReceiptAlreadyInQueueException::class) + fun save(receipt: Receipt): String + + /** + * Returns the printing status of a receipt. + * + * @param receiptId the id of the receipt + * @return the status of the receipt + */ + @Throws(ReceiptNotInQueueException::class) + fun getStatus(receiptId: String): ReceiptStatus + + /** + * Returns a list of exceptions with a given status. + * + * @param receiptStatus the status to group by + * @return the list of receipts with that status + */ + fun getByStatus(receiptStatus: ReceiptStatus): List + + /** + * Marks a receipt as Printed. + * + * @param receiptId the id of the receipt to finish + * @return the finished receipt + */ + @Throws(ReceiptNotInQueueException::class) + fun finishPrinting(receiptId: String): Receipt + + /** + * Marks a receipt as Failed. + * + * @param receiptId the id of the receipt to fail. + * @return the rejected receipt + */ + @Throws(ReceiptNotInQueueException::class) + fun failPrinting(receiptId: String): Receipt + + /** + * Removes a receipt. + * + * @param receiptId the id of the receipt + * @return the removed receipt + */ + @Throws(ReceiptNotInQueueException::class) + fun remove(receiptId: String): Receipt +} + + +internal class ReceiptNotInQueueException : Throwable() +internal class ReceiptAlreadyInQueueException : Throwable() + +enum class ReceiptStatus{ + PRINTING, PRINTED, FAILED +} \ No newline at end of file diff --git a/pos-print/src/test/java/com/clouway/pos/print/adapter/http/AsyncPrintServiceTest.kt b/pos-print/src/test/java/com/clouway/pos/print/adapter/http/AsyncPrintServiceTest.kt new file mode 100644 index 0000000..aa70b40 --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/adapter/http/AsyncPrintServiceTest.kt @@ -0,0 +1,90 @@ +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.* +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 + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class AsyncPrintServiceTest { + + @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 isFiscal = true + + private val receiptWithSource = PrintReceiptRequest(receipt, sourceIp, isFiscal) + + private val receiptDTO = AsyncPrintService + .ReceiptDTO(sourceIp, "::operatorId::", isFiscal, receipt = receipt) + + private val service = AsyncPrintService(repo, queue) + + @Test + fun queueReceiptForPrinting(){ + context.expecting { + oneOf(repo).save(receipt) + will(returnValue(receipt.receiptId)) + + oneOf(queue).queue(receiptWithSource) + } + + val reply = service.printReceipt(FakeRequest.newRequest(receiptDTO)) + MatcherAssert.assertThat(reply, ReplyMatchers.isOk) + MatcherAssert.assertThat(reply, ReplyMatchers.contains(receipt.receiptId)) + } + + @Test + fun savingAlreadyExistingReceiptThrowsException(){ + context.expecting { + oneOf(repo).save(receipt) + will(throwException(ReceiptAlreadyInQueueException())) + } + + val reply = service.printReceipt(FakeRequest.newRequest(receiptDTO)) + MatcherAssert.assertThat(reply, ReplyMatchers.isBadRequest) + } + + @Test + fun getReceiptStatus(){ + context.expecting { + oneOf(repo).getStatus(receipt.receiptId) + will(returnValue(ReceiptStatus.PRINTING)) + } + + val reply = service.getReceiptStatus(receipt.receiptId) + MatcherAssert.assertThat(reply, ReplyMatchers.isOk) + MatcherAssert.assertThat(reply, ReplyMatchers.contains(ReceiptStatus.PRINTING)) + } + + @Test + fun getReceiptStatusOfNonExisting(){ + context.expecting { + oneOf(repo).getStatus(receipt.receiptId) + will(throwException(ReceiptNotInQueueException())) + } + + 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/BackgroundReceiptPrintWorkerTest.kt b/pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorkerTest.kt new file mode 100644 index 0000000..5a2acc8 --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/core/BackgroundReceiptPrintWorkerTest.kt @@ -0,0 +1,168 @@ +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.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import java.io.IOException + +/** + * @author Tsvetozar Bonev (tsvetozar.bonev@clouway.com) + */ +class BackgroundReceiptPrintWorkerTest { + + @Rule + @JvmField + var context = JUnitRuleMockery() + + companion object { + @ClassRule + @JvmField + val dataStoreRule = DatastoreRule() + } + + @Rule + @JvmField + var cleaner = DatastoreCleaner(dataStoreRule.db()) + + + private fun Mockery.expecting(block: Expectations.() -> Unit) { + checking(Expectations().apply(block)) + } + + private val repo = context.mock(ReceiptRepository::class.java) + private val factory = context.mock(PrinterFactory::class.java) + private val notifier = context.mock(PrintNotifier::class.java) + private val queue = context.mock(PrintQueue::class.java) + + private val service = BackgroundReceiptPrintWorker(repo, factory, notifier, queue) + + private val printer = context.mock(ReceiptPrinter::class.java) + + private val receipt = Receipt.newReceipt() + .withReceiptId("::receiptId::") + .withAmount(200.0) + .build() + + private val sourceIp = "::sourceIp::" + private val isFiscal = true + + private val receiptWithSource = PrintReceiptRequest(receipt, sourceIp, isFiscal) + + 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).hasNext() + will(returnValue(true)) + + oneOf(queue).next() + will(returnValue(receiptWithSource)) + + oneOf(factory).getPrinter(sourceIp) + will(returnValue(printer)) + + oneOf(printer).printFiscalReceipt(receipt) + will(returnValue(acceptedPrintingResponse)) + + oneOf(repo).finishPrinting(receipt.receiptId) + will(returnValue(receipt)) + + oneOf(notifier).notifyPrinted(acceptedPrintingResponse) + + oneOf(printer).close() + + oneOf(queue).hasNext() + will(returnValue(false)) + } + + service.printReceipts() + } + + @Test + fun failPrintingReceiptWhenNotPrinted(){ + context.expecting { + oneOf(queue).hasNext() + will(returnValue(true)) + + oneOf(queue).next() + will(returnValue(receiptWithSource)) + + oneOf(factory).getPrinter("::sourceIp::") + will(returnValue(printer)) + + oneOf(printer).printFiscalReceipt(receipt) + will(returnValue(rejectedPrintingResponse)) + + oneOf(repo).failPrinting("::receiptId::") + will(returnValue(receipt)) + + oneOf(notifier).notifyPrinted(rejectedPrintingResponse) + + oneOf(printer).close() + + oneOf(queue).hasNext() + will(returnValue(false)) + } + + service.printReceipts() + } + + @Test + fun failPrintingWhenDeviceNotFound(){ + context.expecting { + oneOf(queue).hasNext() + will(returnValue(true)) + + oneOf(queue).next() + will(returnValue(receiptWithSource)) + + oneOf(factory).getPrinter(sourceIp) + will(throwException(DeviceNotFoundException())) + + oneOf(repo).failPrinting(receipt.receiptId) + will(returnValue(receipt)) + + oneOf(notifier).notifyPrinted(PrintReceiptResponse(setOf(Status.GENERAL_ERROR))) + + oneOf(queue).hasNext() + will(returnValue(false)) + } + + service.printReceipts() + } + + @Test + fun failPrintingWithIOException(){ + context.expecting { + oneOf(queue).hasNext() + will(returnValue(true)) + + oneOf(queue).next() + will(returnValue(receiptWithSource)) + + oneOf(factory).getPrinter(sourceIp) + will(throwException(IOException())) + + oneOf(repo).failPrinting(receipt.receiptId) + will(returnValue(receipt)) + + oneOf(notifier).notifyPrinted(PrintReceiptResponse(setOf(Status.GENERAL_ERROR))) + + oneOf(queue).hasNext() + will(returnValue(false)) + } + + service.printReceipts() + } + +} \ 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..d2f6dc7 --- /dev/null +++ b/pos-print/src/test/java/com/clouway/pos/print/persistent/PersistentReceiptRepositoryTest.kt @@ -0,0 +1,111 @@ +package com.clouway.pos.print.persistent + +import com.clouway.pos.print.adapter.db.* +import com.clouway.pos.print.core.Receipt +import com.clouway.pos.print.core.ReceiptAlreadyInQueueException +import com.clouway.pos.print.core.ReceiptNotInQueueException +import com.clouway.pos.print.core.ReceiptStatus +import com.google.inject.util.Providers +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 + var cleaner = DatastoreCleaner(dataStoreRule.db()) + + private val repository = PersistentReceiptRepository(Providers.of(dataStoreRule.db())) + + private val anyReceipt = Receipt.newReceipt().withReceiptId("::receiptId::").build() + + @Test + fun happyPath() { + repository.save(anyReceipt) + + assertThat(repository.getStatus(anyReceipt.receiptId), Is(ReceiptStatus.PRINTING)) + } + + @Test + fun queueReceipt() { + assertThat(repository.save(anyReceipt), Is(anyReceipt.receiptId)) + } + + @Test(expected = ReceiptAlreadyInQueueException::class) + fun queueingReceiptTwiceThrowsException() { + repository.save(anyReceipt) + repository.save(anyReceipt) + } + + @Test + fun getReceiptStatus() { + repository.save(anyReceipt) + + assertThat(repository.getStatus(anyReceipt.receiptId), Is(ReceiptStatus.PRINTING)) + } + + @Test(expected = ReceiptNotInQueueException::class) + fun gettingStatusOfNonExistingReceiptThrowsException() { + repository.getStatus("::non-existent-receiptId::") + } + + @Test + fun removeReceipt() { + repository.save(anyReceipt) + + assertThat(repository.remove(anyReceipt.receiptId), Is(anyReceipt)) + } + + @Test(expected = ReceiptNotInQueueException::class) + fun removingNonExistingReceiptThrowsException() { + repository.remove("::non-existent-receiptId::") + } + + @Test + fun getReceiptsByStatus() { + repository.save(anyReceipt) + + val printingReceipts = repository.getByStatus(ReceiptStatus.PRINTING) + val printedReceipts = repository.getByStatus(ReceiptStatus.PRINTED) + + assertThat(printingReceipts, Is(listOf(anyReceipt))) + assertThat(printedReceipts, Is(emptyList())) + } + + @Test + fun finishReceipt() { + repository.save(anyReceipt) + val finishedReceipt = repository.finishPrinting(anyReceipt.receiptId) + + assertThat(repository.getStatus(finishedReceipt.receiptId), Is(ReceiptStatus.PRINTED)) + } + + @Test(expected = ReceiptNotInQueueException::class) + fun finishingNonExistingReceiptThrowsException() { + repository.finishPrinting("::fake-receiptId::") + } + + @Test + fun rejectReceipt() { + repository.save(anyReceipt) + val finishedReceipt = repository.failPrinting(anyReceipt.receiptId) + + assertThat(repository.getStatus(finishedReceipt.receiptId), Is(ReceiptStatus.FAILED)) + } + + @Test(expected = ReceiptNotInQueueException::class) + fun rejectingNonExistingReceiptThrowsException() { + repository.finishPrinting("::fake-receiptId::") + } +} \ No newline at end of file