Skip to content

Commit

Permalink
Update medical insurance data from RFZO public API
Browse files Browse the repository at this point in the history
  • Loading branch information
ubavic committed Jun 15, 2024
1 parent b53f25a commit ea52211
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 23 deletions.
60 changes: 60 additions & 0 deletions document/medical.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,28 @@ import (
"errors"
"fmt"
"image"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"

"github.com/signintech/gopdf"
"github.com/ubavic/bas-celik/localization"
)

const rfzoServiceUrl = "https://www.rfzo.rs/proveraUplateDoprinosa2.php"

// Card number doesn't have exactly 11 digits.
var ErrInvalidCardNo = errors.New("invalid card number length")

// Insurance number doesn't have exactly 11 digits.
var ErrInvalidInsuranceNo = errors.New("invalid insurance number length")

// Date `ValidUntil` could not be extracted from RFZO response.
var ErrNoSubmatchFound = errors.New("no submatch found")

// Represents a document stored on a Serbian public medical insurance card.
type MedicalDocument struct {
InsurerName string
Expand Down Expand Up @@ -247,3 +262,48 @@ func (doc *MedicalDocument) BuildPdf() (data []byte, fileName string, retErr err
func (doc *MedicalDocument) BuildJson() ([]byte, error) {
return json.Marshal(doc)
}

func (doc *MedicalDocument) UpdateValidUntilDateFromRfzo() error {
if len([]rune(doc.CardId)) != 11 {
return ErrInvalidCardNo
}

if len([]rune(doc.InsuranceNumber)) != 11 {
return ErrInvalidInsuranceNo
}

resp, err := http.PostForm(rfzoServiceUrl, url.Values{"zk": {doc.CardId}, "lbo": {doc.InsuranceNumber}})
if err != nil {
return fmt.Errorf("posting: %w", err)
}

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading response body: %w", err)
}

date, err := ParseValidUntilDateFromRfzoResponse(string(body))
if err != nil {
return fmt.Errorf("parsing response: %w", err)
}

doc.ValidUntil = date

return nil
}

func ParseValidUntilDateFromRfzoResponse(response string) (string, error) {
regex, err := regexp.Compile(`оверена до: <strong>(\d+\.\d+\.\d+\.)</strong>`)
if err != nil {
return "", fmt.Errorf("compiling regex: %w", err)
}

matches := regex.FindStringSubmatch(response)
if len(matches) < 2 {
return "", ErrNoSubmatchFound
}

return matches[1], nil
}
39 changes: 39 additions & 0 deletions document/medical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var documentMedical2 = document.MedicalDocument{
InsuranceHolderSurnameCyrl: "Петровић",
InsuranceNumber: "12345678",
InsuranceStartDate: "29.03.2014",
CardId: "12345678901",
}
var documentMedical3 = document.MedicalDocument{
GivenName: "Pablo Diego",
Expand Down Expand Up @@ -126,3 +127,41 @@ func Test_BuildPdfMedical(t *testing.T) {
t.Errorf("Unexpected error %v", err)
}
}

func Test_GetExpiryDateFromRfzo(t *testing.T) {
err := documentMedical1.UpdateValidUntilDateFromRfzo()
if err != document.ErrInvalidCardNo {
t.Errorf("Expected the InvalidCardNo error but got %v", err)
}

err = documentMedical2.UpdateValidUntilDateFromRfzo()
if err != document.ErrInvalidInsuranceNo {
t.Errorf("Expected the InvalidInsuranceNo error but got %v", err)
}

}

func Test_parseDateFromRfzoResponse(t *testing.T) {
_, err := document.ParseValidUntilDateFromRfzoResponse("")
if err != document.ErrNoSubmatchFound {
t.Errorf("Expected the NoSubmatchFound error but got %v", err)
}

date, err := document.ParseValidUntilDateFromRfzoResponse("Ваши иницијали су <strong>Н.Н.</strong> (ЛБО: 123456789)<br />Матична филијала: <strong>Београд</strong>.<br/>Ваша здравствена књижица је оверена до: <strong>3.4.2025.</strong>")
if err != nil {
t.Errorf("Expected no error but got %v", err)
}

if date != "3.4.2025." {
t.Errorf("Expected date `3.4.2025.` but got `%s`", date)
}

date, err = document.ParseValidUntilDateFromRfzoResponse("Ваша здравствена књижица је оверена до: <strong>31.12.2023.</strong>")
if err != nil {
t.Errorf("Expected no error but got %v", err)
}

if date != "31.12.2023." {
t.Errorf("Expected date `31.12.2023.` but got `%s`", date)
}
}
10 changes: 8 additions & 2 deletions gui/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"github.com/ubavic/bas-celik/document"
"github.com/ubavic/bas-celik/gui/widgets"
"github.com/ubavic/bas-celik/localization"
Expand Down Expand Up @@ -78,7 +79,7 @@ func pageMedical(doc *document.MedicalDocument) *fyne.Container {
issueDateF := widgets.NewField("Datum izdavanja", doc.CardIssueDate, 170)
expiryDateF := widgets.NewField("Datum važenja", doc.CardExpiryDate, 170)
cardRow1 := container.New(layout.NewHBoxLayout(), issueDateF, expiryDateF)
validUntilF := widgets.NewField("Overena do", doc.ValidUntil, 170)
validUntilF := widgets.NewField("Overena do*", doc.ValidUntil, 170)
permanentlyValidF := widgets.NewField("Trajna overa", localization.FormatYesNo(doc.PermanentlyValid, localization.Latin), 170)
cardRow2 := container.New(layout.NewHBoxLayout(), validUntilF, permanentlyValidF)
cardGroup := widgets.NewGroup("Podaci o kartici", cardRow1, cardRow2)
Expand All @@ -94,7 +95,12 @@ func pageMedical(doc *document.MedicalDocument) *fyne.Container {

colRight := container.New(layout.NewVBoxLayout(), insuranceHolderGroup, cardGroup, obligeeGroup)

return container.New(layout.NewHBoxLayout(), colLeft, colRight)
note := widget.NewLabel("* Datum isteka overe ne mora da bude ažuran na kartici. " +
"Pritiskom na dugme Ažuriraj, ovaj podatak će se ažurirati sa podatkom dostupnim na web servisu RFZO-a. " +
"Ova akcija zahteva konekciju sa internetom.")
note.Wrapping = fyne.TextWrapWord

return container.New(layout.NewVBoxLayout(), container.New(layout.NewHBoxLayout(), colLeft, colRight), note)
}

func pageVehicle(doc *document.VehicleDocument) *fyne.Container {
Expand Down
44 changes: 32 additions & 12 deletions gui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import (
)

type State struct {
mu sync.Mutex
startPageOn bool
verbose bool
window *fyne.Window
startPage *widgets.StartPage
toolbar *widgets.Toolbar
spacer *widgets.Spacer
statusBar *widgets.StatusBar
mu sync.Mutex
startPageOn bool
verbose bool
window *fyne.Window
startPage *widgets.StartPage
toolbar *widgets.Toolbar
spacer *widgets.Spacer
statusBar *widgets.StatusBar
medicalUpdateButton *widget.Button
}

var state State
Expand Down Expand Up @@ -64,20 +65,26 @@ func setUI(doc document.Document) {
state.mu.Lock()
defer state.mu.Unlock()

pdfHandler := savePdf(doc)
saveButton := widget.NewButton("Sačuvaj PDF", pdfHandler)
buttonBar := container.New(layout.NewHBoxLayout(), state.statusBar, layout.NewSpacer(), saveButton)

var page *fyne.Container
buttonBarObjects := []fyne.CanvasObject{state.statusBar, layout.NewSpacer()}

switch doc := doc.(type) {
case *document.IdDocument:
page = pageID(doc)
case *document.MedicalDocument:
updateButton := widget.NewButton("Ažuriraj", updateMedicalDocHandler(doc))
buttonBarObjects = append(buttonBarObjects, updateButton)
page = pageMedical(doc)
case *document.VehicleDocument:
page = pageVehicle(doc)
}

pdfHandler := savePdf(doc)
saveButton := widget.NewButton("Sačuvaj PDF", pdfHandler)
buttonBarObjects = append(buttonBarObjects, saveButton)

buttonBar := container.New(layout.NewHBoxLayout(), buttonBarObjects...)

rows := container.New(layout.NewVBoxLayout(), state.toolbar, state.spacer, page, buttonBar)
columns := container.New(layout.NewHBoxLayout(), layout.NewSpacer(), rows, layout.NewSpacer())
container := container.New(layout.NewPaddedLayout(), columns)
Expand Down Expand Up @@ -178,3 +185,16 @@ func ShowAboutBox(win fyne.Window, version string) func() {
)
}
}

func updateMedicalDocHandler(doc *document.MedicalDocument) func() {
return func() {
err := doc.UpdateValidUntilDateFromRfzo()
if err != nil {
dialog.ShowInformation("Greška", "Greška prilikom ažuriranja podataka", *state.window)
return
}

dialog.ShowInformation("Ažuriranje", "Ažuriranje podataka je uspešno izvršeno", *state.window)
setUI(doc)
}
}
4 changes: 3 additions & 1 deletion internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import (

var version string

func ProcessFlags(_jsonPath, _pdfPath *string, _verbose *bool, _readerIndex *uint) bool {
func ProcessFlags(_jsonPath, _pdfPath *string, _verbose, _getMedicalExpiryDateFromRfzo *bool, _readerIndex *uint) bool {
atrFlag := flag.Bool("atr", false, "Print the ATR form the card and exit")
jsonPath := flag.String("json", "", "Set JSON export path")
listFlag := flag.Bool("list", false, "List connected readers and exit")
pdfPath := flag.String("pdf", "", "Set PDF export path.")
getMedicalExpiryDateFromRfzo := flag.Bool("rfzoExpiryDate", false, "Get expiry date of medical card from RFZO API. Ignored for other cards")
verboseFlag := flag.Bool("verbose", false, "Provide additional details in the terminal")
versionFlag := flag.Bool("version", false, "Display version information and exit")
readerIndex := flag.Uint("reader", 0, "Set reader")
Expand Down Expand Up @@ -47,6 +48,7 @@ func ProcessFlags(_jsonPath, _pdfPath *string, _verbose *bool, _readerIndex *uin
*_pdfPath = *pdfPath
*_verbose = *verboseFlag
*_readerIndex = *readerIndex
*_getMedicalExpiryDateFromRfzo = *getMedicalExpiryDateFromRfzo

return false
}
Expand Down
13 changes: 12 additions & 1 deletion internal/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (

"github.com/ebfe/scard"
"github.com/ubavic/bas-celik/card"
"github.com/ubavic/bas-celik/document"
)

func readAndSave(pdfPath, jsonPath string, reader uint) error {
func readAndSave(pdfPath, jsonPath string, reader uint, getMedicalExpiryDateFromRfzo bool) error {
ctx, err := scard.EstablishContext()
if err != nil {
return fmt.Errorf("establishing context: %w", err)
Expand Down Expand Up @@ -54,6 +55,16 @@ func readAndSave(pdfPath, jsonPath string, reader uint) error {
return fmt.Errorf("reading card: %w", err)
}

switch doc := doc.(type) {
case *document.MedicalDocument:
if getMedicalExpiryDateFromRfzo {
err := doc.UpdateValidUntilDateFromRfzo()
if err != nil {
return fmt.Errorf("updating `ValidUntil` date: %w", err)
}
}
}

if len(pdfPath) > 0 {
pdf, _, err := doc.BuildPdf()
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/runCLI.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

package internal

func Run(pdfPath, jsonPath string, verbose bool, reader uint) error {
func Run(pdfPath, jsonPath string, verbose, getMedicalExpiryDateFromRfzo bool, reader uint) error {
if len(pdfPath) == 0 && len(jsonPath) == 0 {
jsonPath = "out.json"
}

return readAndSave(pdfPath, jsonPath, reader)
return readAndSave(pdfPath, jsonPath, reader, getMedicalExpiryDateFromRfzo)
}
4 changes: 2 additions & 2 deletions internal/runGUI.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"github.com/ubavic/bas-celik/gui"
)

func Run(pdfPath, jsonPath string, verbose bool, reader uint) error {
func Run(pdfPath, jsonPath string, verbose, getMedicalExpiryDateFromRfzo bool, reader uint) error {
if len(pdfPath) == 0 && len(jsonPath) == 0 {
gui.StartGui(verbose, version)
return nil
} else {
return readAndSave(pdfPath, jsonPath, reader)
return readAndSave(pdfPath, jsonPath, reader, getMedicalExpiryDateFromRfzo)
}
}
6 changes: 3 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ func main() {
internal.SetVersion(version)

var jsonPath, pdfPath string
var verboseFlag bool
var verboseFlag, getMedicalExpiryDateFromRfzo bool
var readerIndex uint
exit := internal.ProcessFlags(&jsonPath, &pdfPath, &verboseFlag, &readerIndex)
exit := internal.ProcessFlags(&jsonPath, &pdfPath, &verboseFlag, &getMedicalExpiryDateFromRfzo, &readerIndex)
if exit {
return
}
Expand All @@ -31,7 +31,7 @@ func main() {
return
}

err = internal.Run(pdfPath, jsonPath, verboseFlag, readerIndex)
err = internal.Run(pdfPath, jsonPath, verboseFlag, getMedicalExpiryDateFromRfzo, readerIndex)
if err != nil {
fmt.Println("Error saving document:", err)
}
Expand Down

0 comments on commit ea52211

Please sign in to comment.