Compare commits
6 Commits
1.0.11
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
efa415e631 | |||
f1dc0ccce4 | |||
1bceb55285 | |||
bb8cdb1d84 | |||
251afd6aeb | |||
e6ff292706 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ Checkin.code-workspace
|
||||
_obj
|
||||
_test
|
||||
.vscode/
|
||||
ChipDNAClient/
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
|
@ -251,6 +251,8 @@ func CardToEncoderPosition(port *serial.Port) (string, error) {
|
||||
return "", fmt.Errorf("error sending ENQ to prompt device: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
|
||||
//Check card position status
|
||||
status, err := CheckDispenserStatus(port)
|
||||
if err != nil {
|
||||
@ -281,6 +283,8 @@ func CardOutOfMouth(port *serial.Port) (string, error) {
|
||||
return "", fmt.Errorf("error sending ENQ to prompt device: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
|
||||
//Check card position status
|
||||
status, err := CheckDispenserStatus(port)
|
||||
if err != nil {
|
||||
|
@ -19,8 +19,15 @@ func (lock *AssaLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
||||
}
|
||||
|
||||
// Checks heart beat of the Assa Abloy lock server and perform key encoding
|
||||
func (lock *AssaLockServer) LockSequence(conn net.Conn) error {
|
||||
func (lock *AssaLockServer) LockSequence() error {
|
||||
const funcName = "AssaLockServer.LockSequence"
|
||||
|
||||
conn, err := InitializeServerConnection(LockServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
resp, err := sendHeartbeatToServer(conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] heartbeat failed: %v", funcName, err)
|
||||
|
@ -35,7 +35,7 @@ var (
|
||||
type (
|
||||
LockServer interface {
|
||||
BuildCommand(doorReq DoorCardRequest, checkIn, checkOut time.Time) error
|
||||
LockSequence(conn net.Conn) error
|
||||
LockSequence() error
|
||||
}
|
||||
|
||||
AssaLockServer struct {
|
||||
|
@ -53,8 +53,15 @@ func (lock *OmniLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, check
|
||||
}
|
||||
|
||||
// Starts link to the Omnitec lock server and perform key encoding
|
||||
func (lock *OmniLockServer) LockSequence(conn net.Conn) error {
|
||||
func (lock *OmniLockServer) LockSequence() error {
|
||||
const funcName = "OmniLockServer.LockSequence"
|
||||
|
||||
conn, err := InitializeServerConnection(LockServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Start the link with the lock server
|
||||
regs, err := lock.linkStart(conn)
|
||||
if err != nil {
|
||||
|
@ -139,14 +139,21 @@ func (lock *SaltoLockServer) waitForAck(conn net.Conn, reader *bufio.Reader, tim
|
||||
}
|
||||
|
||||
// LockSequence performs the full ENQ/ACK handshake and command exchange
|
||||
func (lock *SaltoLockServer) LockSequence(conn net.Conn) error {
|
||||
func (lock *SaltoLockServer) LockSequence() error {
|
||||
const timeout = 10 * time.Second
|
||||
var (
|
||||
resp []byte
|
||||
reader = bufio.NewReader(conn)
|
||||
drained = 0 // count of stale frames consumed across waits
|
||||
)
|
||||
|
||||
conn, err := InitializeServerConnection(LockServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// 1. Send ENQ
|
||||
log.Infof("Sending ENQ")
|
||||
if _, e := conn.Write([]byte{ENQ}); e != nil {
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@ -36,9 +35,9 @@ func (lock *TLJLockServer) BuildCommand(doorReq DoorCardRequest, checkIn, checkO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lock *TLJLockServer) LockSequence(conn net.Conn) error {
|
||||
func (lock *TLJLockServer) LockSequence() error {
|
||||
log.Infof("Sending command: %q", lock.command)
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(lock.command)
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request failed: %v", err)
|
||||
|
96
main.go
96
main.go
@ -1,12 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// "database/sql"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -30,7 +29,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
buildVersion = "1.0.11"
|
||||
buildVersion = "1.0.18"
|
||||
serviceName = "hardlink"
|
||||
customLayout = "2006-01-02 15:04:05 -0700"
|
||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||
@ -52,15 +51,15 @@ type configRec struct {
|
||||
|
||||
// App holds shared resources.
|
||||
type App struct {
|
||||
configRec configRec
|
||||
dispPort *serial.Port
|
||||
lockConn net.Conn
|
||||
lockserver lockserver.LockServer
|
||||
}
|
||||
|
||||
func newApp(dispPort *serial.Port, lockConn net.Conn, config configRec) *App {
|
||||
func newApp(dispPort *serial.Port, config configRec) *App {
|
||||
return &App{
|
||||
configRec: config,
|
||||
dispPort: dispPort,
|
||||
lockConn: lockConn,
|
||||
lockserver: lockserver.NewLockServer(config.LockType, config.EncoderAddress, fatalError),
|
||||
}
|
||||
}
|
||||
@ -70,8 +69,6 @@ func main() {
|
||||
config := readConfig()
|
||||
printer.Layout = readTicketLayout()
|
||||
printer.PrinterName = config.PrinterName
|
||||
|
||||
var lockConn net.Conn
|
||||
lockserver.Cert = config.Cert
|
||||
lockserver.LockServerURL = config.LockserverUrl
|
||||
|
||||
@ -103,25 +100,19 @@ func main() {
|
||||
}
|
||||
log.Infof("Dispenser initialized on port %s, %s", config.DispenserPort, status)
|
||||
|
||||
// Initialize lock-server connection once
|
||||
// Test lock-server connection
|
||||
switch strings.ToLower(config.LockType) {
|
||||
case lockserver.TLJ:
|
||||
|
||||
default:
|
||||
lockConn, err = lockserver.InitializeServerConnection(config.LockserverUrl)
|
||||
lockConn, err := lockserver.InitializeServerConnection(config.LockserverUrl)
|
||||
if err != nil {
|
||||
fatalError(err)
|
||||
}
|
||||
defer lockConn.Close()
|
||||
log.Infof("Connected to lock server at %s", config.LockserverUrl)
|
||||
log.Infof("Connectting to lock server at %s", config.LockserverUrl)
|
||||
lockConn.Close()
|
||||
}
|
||||
|
||||
// db, err := payment.InitMSSQL(config.dbport, config.dbname, config.dbuser, config.dbpassword)
|
||||
// if err != nil {
|
||||
// fatalError(fmt.Errorf("DB init failed: %v", err))
|
||||
// }
|
||||
// defer db.Close()
|
||||
|
||||
if config.IsPayment {
|
||||
startClient := func() (*exec.Cmd, error) {
|
||||
cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe")
|
||||
@ -181,7 +172,7 @@ func main() {
|
||||
|
||||
// Create App and wire routes
|
||||
// dispHandle := &serial.Port{} // Placeholder, replace with actual dispenser handle
|
||||
app := newApp(dispHandle, lockConn, config)
|
||||
app := newApp(dispHandle, config)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
setUpRoutes(app, mux)
|
||||
@ -241,18 +232,36 @@ func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
json.NewEncoder(w).Encode(theResponse)
|
||||
}
|
||||
|
||||
func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmstypes.ResponseRec) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(theResponse); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "JSON encode error", "startTransaction", "", "", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("startTransaction")
|
||||
var (
|
||||
theResponse cmstypes.ResponseRec
|
||||
cardholderReceipt string
|
||||
theRequest cmstypes.TransactionRec
|
||||
)
|
||||
|
||||
theResponse.Status.Code = http.StatusInternalServerError
|
||||
theResponse.Status.Message = "500 Internal server error"
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if !app.configRec.IsPayment {
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Payment processing is disabled")
|
||||
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
@ -260,29 +269,43 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Println("startTransaction called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Method not allowed; use POST")
|
||||
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if ct := r.Header.Get("Content-Type"); ct != "text/xml" {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be text/xml")
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Content-Type must be text/xml")
|
||||
writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse)
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 90 * time.Second}
|
||||
response, err := client.Post(transactionUrl, "text/xml", r.Body)
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
err := xml.Unmarshal(body, &theRequest)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Invalid XML payload")
|
||||
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
||||
return
|
||||
}
|
||||
log.Printf("Start trnasaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType)
|
||||
|
||||
client := &http.Client{Timeout: 300 * time.Second}
|
||||
response, err := client.Post(transactionUrl, "text/xml", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "Payment processing failed: "+err.Error())
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "No response from payment processor")
|
||||
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
body, err = io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Read response body error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "Failed to read response body: "+err.Error())
|
||||
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Failed to read response body")
|
||||
writeTransactionResult(w, http.StatusInternalServerError, theResponse)
|
||||
return
|
||||
}
|
||||
|
||||
@ -292,10 +315,11 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
result := make(map[string]string)
|
||||
for _, e := range responseEntries {
|
||||
switch e.Key {
|
||||
case "RECEIPT_DATA", "RECEIPT_DATA_MERCHANT":
|
||||
case "RECEIPT_DATA_CARDHOLDER":
|
||||
case payment.ReceiptData, payment.ReceiptDataMerchant:
|
||||
// ignore these
|
||||
case payment.ReceiptDataCardholder:
|
||||
cardholderReceipt = e.Value
|
||||
case "TRANSACTION_RESULT":
|
||||
case payment.TransactionResult:
|
||||
theResponse.Status.Message = e.Value
|
||||
theResponse.Status.Code = http.StatusOK
|
||||
result[e.Key] = e.Value
|
||||
@ -308,18 +332,8 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
log.Errorf("PrintCardholderReceipt error: %v", err)
|
||||
}
|
||||
|
||||
// Insert into DB
|
||||
// if err := payment.InsertTransactionRecord(r.Context(), app.db, result); err != nil {
|
||||
// log.Errorf("DB insert error: %v", err)
|
||||
// }
|
||||
|
||||
theResponse.Data = payment.BuildRedirectURL(result)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(theResponse); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "JSON encode error", string(op), "", "", 0)
|
||||
}
|
||||
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||
}
|
||||
|
||||
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||
@ -389,7 +403,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
||||
|
||||
// lock server sequence
|
||||
err = app.lockserver.LockSequence(app.lockConn)
|
||||
err = app.lockserver.LockSequence()
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
|
@ -16,6 +16,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// Transaction types
|
||||
SaleTransactionType = "sale"
|
||||
AccountVerificationType = "account verification"
|
||||
|
||||
// Transaction results
|
||||
ResultApproved = "approved"
|
||||
ResultDeclined = "declined"
|
||||
ResultCancelled = "cancelled"
|
||||
@ -23,6 +28,24 @@ const (
|
||||
ResultError = "error"
|
||||
CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment
|
||||
CheckinUnsuccessfulEndpoint = "/unsuccessful"
|
||||
|
||||
// Response map keys
|
||||
CardReference = "CARD_REFERENCE"
|
||||
CardHash = "CARD_HASH"
|
||||
Errors = "ERRORS"
|
||||
ReceiptData = "RECEIPT_DATA"
|
||||
ReceiptDataMerchant = "RECEIPT_DATA_MERCHANT"
|
||||
ReceiptDataCardholder = "RECEIPT_DATA_CARDHOLDER"
|
||||
Reference = "REFERENCE"
|
||||
TransactionResult = "TRANSACTION_RESULT"
|
||||
TransactionType = "TRANSACTION_TYPE"
|
||||
ConfirmResult = "CONFIRM_RESULT"
|
||||
ConfirmErrors = "CONFIRM_ERRORS"
|
||||
|
||||
// Log field keys
|
||||
LogFieldError = "error"
|
||||
LogFieldDescription = "description"
|
||||
LogResult = "transactionResult"
|
||||
)
|
||||
|
||||
// XML parsing structs
|
||||
@ -42,6 +65,14 @@ type (
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
TransactionConfirmation struct {
|
||||
XMLName xml.Name `xml:"TransactionConfirmation"`
|
||||
Result string `xml:"Result"`
|
||||
Errors string `xml:"Errors"`
|
||||
ErrorDescription string `xml:"ErrorDescription"`
|
||||
ReceiptDataCardholder string `xml:"ReceiptDataCardholder"`
|
||||
}
|
||||
)
|
||||
|
||||
// ParseTransactionResult parses the XML into entries.
|
||||
@ -217,65 +248,70 @@ func nullableFloatArg(nf sql.NullFloat64) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildRedirectURL builds the redirect URL to send the guest to after payment.
|
||||
func BuildRedirectURL(result map[string]string) string {
|
||||
// 1) normalize the result code
|
||||
res := strings.ToLower(result["TRANSACTION_RESULT"])
|
||||
res := result[TransactionResult]
|
||||
tType := result[TransactionType]
|
||||
|
||||
// 2) pick base path and optional error params
|
||||
var basePath string
|
||||
var msgType, description string
|
||||
// Transaction approved?
|
||||
if strings.EqualFold(res, ResultApproved) {
|
||||
switch {
|
||||
// Transaction type AccountVerification?
|
||||
case strings.EqualFold(tType, AccountVerificationType):
|
||||
log.WithField(LogResult, result[TransactionResult]).
|
||||
Info("Account verification approved")
|
||||
|
||||
switch res {
|
||||
case ResultApproved:
|
||||
basePath = CheckinSuccessfulEndpoint
|
||||
return buildSuccessURL(result)
|
||||
|
||||
case ResultDeclined:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "declined"
|
||||
description = "payment declined"
|
||||
// Transaction type Sale?
|
||||
case strings.EqualFold(tType, SaleTransactionType):
|
||||
// Transaction confirmed?
|
||||
if strings.EqualFold(result[ConfirmResult], ResultApproved) {
|
||||
log.WithField(LogResult, result[ConfirmResult]).
|
||||
Info("Transaction approved and confirmed")
|
||||
|
||||
case ResultCancelled:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "cancelled"
|
||||
description = "payment cancelled by customer"
|
||||
return buildSuccessURL(result)
|
||||
}
|
||||
|
||||
case ResultPending:
|
||||
// you could choose to treat pending as unsuccessful or special-case it
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "pending"
|
||||
description = "payment pending"
|
||||
case ResultError:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "error"
|
||||
description = result["ERROR"]
|
||||
// Not confirmed
|
||||
log.WithFields(log.Fields{LogFieldError: result[ConfirmResult], LogFieldDescription: result[ConfirmErrors]}).
|
||||
Error("Transaction approved but not confirmed")
|
||||
|
||||
default:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "error"
|
||||
description = "unknown transaction result"
|
||||
return BuildFailureURL(result[ConfirmResult], result[ConfirmErrors])
|
||||
}
|
||||
}
|
||||
|
||||
if msgType != "" {
|
||||
log.Warnf("Transaction %s: %s - %s", res, msgType, description)
|
||||
}
|
||||
|
||||
// 3) build query params
|
||||
q := url.Values{}
|
||||
q.Set("TxnReference", result["REFERENCE"])
|
||||
q.Set("CardHash", hex.EncodeToString([]byte(result["CARD_HASH"])))
|
||||
q.Set("CardReference", hex.EncodeToString([]byte(result["CARD_REFERENCE"])))
|
||||
|
||||
// only append these when non-approved
|
||||
if msgType != "" {
|
||||
q.Set("MsgType", msgType)
|
||||
q.Set("Description", description)
|
||||
}
|
||||
|
||||
// 4) assemble final URL
|
||||
// note: url.URL automatically escapes values in RawQuery
|
||||
u := url.URL{
|
||||
Path: basePath,
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
// Not approved
|
||||
return BuildFailureURL(res, result[Errors])
|
||||
}
|
||||
|
||||
func buildSuccessURL(result map[string]string) string {
|
||||
q := url.Values{}
|
||||
q.Set("TxnReference", result[Reference])
|
||||
q.Set("CardHash", hex.EncodeToString([]byte(result[CardHash])))
|
||||
q.Set("CardReference", hex.EncodeToString([]byte(result[CardReference])))
|
||||
return (&url.URL{
|
||||
Path: CheckinSuccessfulEndpoint,
|
||||
RawQuery: q.Encode(),
|
||||
}).String()
|
||||
}
|
||||
|
||||
func BuildFailureURL(msgType, description string) string {
|
||||
q := url.Values{}
|
||||
if msgType == "" {
|
||||
msgType = ResultError
|
||||
}
|
||||
if description == "" {
|
||||
description = "Transaction failed"
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{LogFieldError: msgType, LogFieldDescription: description}).
|
||||
Error("Transaction failed")
|
||||
|
||||
q.Set("MsgType", msgType)
|
||||
q.Set("Description", description)
|
||||
return (&url.URL{
|
||||
Path: CheckinUnsuccessfulEndpoint,
|
||||
RawQuery: q.Encode(),
|
||||
}).String()
|
||||
}
|
||||
|
@ -2,38 +2,56 @@
|
||||
|
||||
builtVersion is a const in main.go
|
||||
|
||||
#### 1.0.11 - 11 August 2024
|
||||
#### 1.0.18 - 04 September 2025
|
||||
increased timeout for TLJ lock server connection to 30 seconds
|
||||
|
||||
#### 1.0.17 - 30 August 2025
|
||||
added functionality to commit transactions
|
||||
|
||||
#### 1.0.15 - 27 August 2025
|
||||
fixed TCP/IP connection to the lock server
|
||||
|
||||
#### 1.0.14 - 21 August 2025
|
||||
fixed issue in creditcall payment processing where error description was not properly set
|
||||
|
||||
#### 1.0.13 - 21 August 2025
|
||||
TCP/IP connection to the lock server is now established before encoding the keycard and closedafter the encoding is done.
|
||||
|
||||
#### 1.0.12 - 11 August 2025
|
||||
added delay before checking dispenser status
|
||||
|
||||
#### 1.0.11 - 11 August 2025
|
||||
updated Salto key encoding workflow
|
||||
|
||||
#### 1.0.10 - 08 August 2024
|
||||
#### 1.0.10 - 08 August 2025
|
||||
updated logging for TLJ locks
|
||||
|
||||
#### 1.0.9 - 08 August 2024
|
||||
#### 1.0.9 - 08 August 2025
|
||||
added TLJ lock server and implemented workflow for TLJ locks
|
||||
|
||||
#### 1.0.8 - 01 August 2024
|
||||
#### 1.0.8 - 01 August 2025
|
||||
improved error handling and logging in Salto
|
||||
|
||||
#### 1.0.7 - 25 July 2024
|
||||
#### 1.0.7 - 25 July 2025
|
||||
added check if the room exists
|
||||
|
||||
#### 1.0.6 - 25 July 2024
|
||||
#### 1.0.6 - 25 July 2025
|
||||
updated workflow for Salto locks
|
||||
|
||||
#### 1.0.5 - 24 July 2024
|
||||
#### 1.0.5 - 24 July 2025
|
||||
added encoding keycard copy for Salto locks
|
||||
|
||||
#### 1.0.4 - 22 July 2024
|
||||
#### 1.0.4 - 22 July 2025
|
||||
added salto lock server and implemented workflow for Salto
|
||||
|
||||
#### 1.0.0 - 30 June 2024
|
||||
#### 1.0.0 - 30 June 2025
|
||||
added creditcall payment method
|
||||
`/starttransaction` - API payment endpoint to start a transaction
|
||||
|
||||
#### 0.9.1 - 22 May 2024
|
||||
#### 0.9.1 - 22 May 2025
|
||||
added lockserver interface and implemented workflow for Omnitec
|
||||
|
||||
#### 0.9.0 - 22 May 2024
|
||||
#### 0.9.0 - 22 May 2025
|
||||
The new API has two new endpoints:
|
||||
- `/issuedoorcard` - encoding the door card for the room.
|
||||
- `/printroomticket` - printing the room ticket.
|
Loading…
x
Reference in New Issue
Block a user