All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jlangch.venice.qrref.venice Maven / Gradle / Ivy

;;;;   __    __         _
;;;;   \ \  / /__ _ __ (_) ___ ___
;;;;    \ \/ / _ \ '_ \| |/ __/ _ \
;;;;     \  /  __/ | | | | (_|  __/
;;;;      \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; Copyright 2017-2024 Venice
;;;;
;;;; Licensed under the Apache License, Version 2.0 (the "License");
;;;; you may not use this file except in compliance with the License.
;;;; You may obtain a copy of the License at
;;;;
;;;;     http://www.apache.org/licenses/LICENSE-2.0
;;;;
;;;; Unless required by applicable law or agreed to in writing, software
;;;; distributed under the License is distributed on an "AS IS" BASIS,
;;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;;; See the License for the specific language governing permissions and
;;;; limitations under the License.

;;;; Venice QR Reference functions


(ns qrref)


(def bill-types { :bill        0
                  :reminder-1  1
                  :reminder-2  2
                  :reminder-3  3 })


;; the number of meta digits in the QR reference:
;;    - bill type digit
;;    - version digit
;;    - checksum digit
(def- num-meta-chars 3)

(def- qr-ref-len 27)

(def- checksum-table [0, 9, 4, 6, 8, 2, 7, 1, 3, 5])


(defn- digit->long [c] (- (long c) (long #\0)))


(defn- long->bill-type [n]
  (if-let [t (get (map-invert bill-types) n)]
    t
    (throw (ex :VncException (str "Invalid bill type index '" n "'!")))))


(defn- bill-type->long [t]
  (if-let [n (get bill-types t)]
    n
    (throw (ex :VncException (str "Invalid bill type '" (pr-str t) "'!")))))


(defn- remove-leading-zeroes [s]
  (loop [s s]
    (if (and (> (count s) 1) (str/starts-with? s "0"))
      (recur (str/rest s))
      s)))


(defn mod-10-checksum [s]
  (if-not (match? s #"[ 0-9]+")
    (throw (ex :VncException "The string must only contain spaces and digits!"))
    (reduce (fn [carry d] (get checksum-table (mod (+ carry (digit->long d)) 10)))
            0
            (filter str/digit? (seq s)))))


(defn- qr-ref-raw [version bill-type bill-nr]
  (let [padding-zeros (- qr-ref-len num-meta-chars (count bill-nr))]
    (if (neg? padding-zeros)
      (throw (ex :VncException "The QR-Reference bill number is too long!"))
      (str (str/repeat "0" padding-zeros)
           bill-nr
           (bill-type->long bill-type)
           version))))


(defn
  ^{ :arglists '("(format s)")
     :doc "Format a QR reference."
     :examples '(
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/format "000000000000000000001234011"))
         """,
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/format (qr/qr-ref 1 :bill "1234")))
         """ )
     :see-also '("qrref/qr-ref", "qrref/parse") }

  format [s]

  (->> (seq s)
       (reverse)
       (partition-all 5)
       (map #(apply str (reverse %)))
       (reverse)
       (str/join " ")))

(defn
  ^{ :arglists '("(qr-ref version bill-type bill-nr)")
     :doc """
          Creates a QR reference according to the Swiss payment standards.

            - *version*, an integer [1..9]¶
            - *bill-type*, one of {:bill, :reminder-1, :reminder-2, :reminder-3}¶
            - *bill-nr*, a string with up to 24 digits '0'..'9'

          [Swiss Payment Standards / de](https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-de.pdf)

          [Swiss Payment Standards / en](https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf)
          """
     :examples '(
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/qr-ref 1 :bill "1234"))
         """ )
     :see-also '("qrref/parse", "qrref/format") }

  qr-ref [version bill-type bill-nr]

  (when-not (< 0 version 10)
    (throw (ex :VncException
               (str/format "Unsupported QR-Reference version %d! Must be [1..9]."
                           version))))

  (let [ref (qr-ref-raw version bill-type bill-nr)]
    (str ref (mod-10-checksum ref))))


(defn
  ^{ :arglists '("(parse ref)")
     :doc "Parse a QR reference. The reference may be formatted."
     :examples '(
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/parse (qr/qr-ref 1 :bill "1234")))
         """,
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/parse "000000000000000000001234011"))
         """,
         """
         (do
           (load-module :qrref ['qrref :as 'qr])
           (qr/parse "00 00000 00000 00000 00012 34011"))
         """ )
     :see-also '("qrref/qr-ref", "qrref/format") }

  parse [ref]

  (when (str/blank? ref)
    (throw (ex :VncException "A QR-Reference must not be blank!")))

  (when-not (match? ref #"[ 0-9]+")
    (throw (ex :VncException
               "A QR-Reference must be built from spaces and digits only!")))

  (let [ref-norm (str/replace-all ref " " "")]
    (when (< (count ref-norm) 10)
      (throw (ex :VncException "A QR-Reference must have more than 10 digits!")))

    (let [meta      (str/nlast ref-norm 3)
          type      (digit->long (first meta))
          version   (digit->long (second meta))
          check     (digit->long (third meta))
          raw-ref   (str/butlast ref-norm)
          check-eff (mod-10-checksum raw-ref)
          bill-nr   (str/butnlast ref-norm 3)]
      (when-not (= check check-eff)
        (throw (ex :VncException
                   (str/format (str "Invalid QR-Reference checksum '%d' for ref "
                                    "'%s'. The effective checksum is '%d'!")
                   check
                   raw-ref
                   check-eff))))
     { :version  version
       :bill-typ (long->bill-type type)
       :bill-nr  (remove-leading-zeroes bill-nr) })))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy