Skip to content

Commit

Permalink
pos-print: add async receipt printing
Browse files Browse the repository at this point in the history
  • Loading branch information
tsbonev committed Sep 20, 2018
1 parent f1178f9 commit eb9fac6
Show file tree
Hide file tree
Showing 17 changed files with 800 additions and 3 deletions.
9 changes: 6 additions & 3 deletions pos-print/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.1.1'
ext.kotlin_version = '1.2.61'
repositories {
mavenCentral()
jcenter()
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(() -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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 ([email protected])
*/
class PersistentReceiptRepository @Inject constructor(private val database: Provider<MongoDatabase>)
: ReceiptRepository {

private val collectionName: String = "receipts"

override fun register(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 getByStatus(receiptStatus: ReceiptStatus): List<Receipt> {
val receiptDocs = receipts().find(eq("status", receiptStatus.name))

val receiptList = mutableListOf<Receipt>()

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<Document> {
return database.get().getCollection(collectionName)
}

@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<String>)
.suffixLines(this["suffixLines"] as List<String>)

val itemList = this["receiptItems"] as List<Document>

itemList.forEach {
receipt.addItem(it.toReceiptItem())
}

return receipt.build()
}

private fun Document.toReceiptItem(): ReceiptItem{
return ReceiptItem.newItem()
.name(this.getString("name"))
.price(this.getDouble("price"))
.quantity(this.getDouble("quantity"))
.build()
}

private fun Receipt.toDocument(): Document{
val itemList = this.receiptItems

val docList = mutableListOf<Document>()

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)
}

private fun ReceiptItem.toDocument(): Document{
return Document()
.append("name", this.name)
.append("price", this.price)
.append("quantity", this.quantity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(PrintServiceV2.class);
at("/v1/devices").serve(DeviceConfigurationService.class);
at("/v1/reports").serve(ReportService.class);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
*/
@Service
@At("/v2/receipts/req/print")
class PrintServiceV2 @Inject constructor(private var repository: ReceiptRepository,
private var queue: PrintQueue) {
private val logger = LoggerFactory.getLogger(PrintServiceV2::class.java)

@Post
fun printReceipt(request: Request): Reply<*> {
return try {
val dto = request.read(ReceiptDTO::class.java).`as`(GsonTransport::class.java)
val receiptId = repository.register(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<Int>().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<Int>().notFound()
}
}

internal data class ReceiptDTO(val sourceIp: String = "", val operatorId: String = "", val fiscal: Boolean = false, val receipt: Receipt = Receipt.newReceipt().build())
}
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
*/
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.clouway.pos.print.core

/**
* @author Tsvetozar Bonev ([email protected])
*/
class ConsolePrintNotifier : PrintNotifier {
override fun notifyPrinted(printResponse: PrintReceiptResponse) {
Thread.sleep(1000)
println(printResponse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.clouway.pos.print.core

import org.eclipse.jetty.util.BlockingArrayQueue

/**
* @author Tsvetozar Bonev ([email protected])
*/
class InMemoryPrintQueue : PrintQueue {

private val queue = BlockingArrayQueue<PrintReceiptRequest>()

/**
* 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)
}
}
Loading

0 comments on commit eb9fac6

Please sign in to comment.