diff --git a/api/compliance.go b/api/compliance.go
new file mode 100644
index 00000000..23116aa6
--- /dev/null
+++ b/api/compliance.go
@@ -0,0 +1,58 @@
+package api
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/Gleipnir-Technology/nidus-sync/platform"
+ "github.com/Gleipnir-Technology/nidus-sync/platform/imagetile"
+ "github.com/go-chi/chi/v5"
+ "github.com/rs/zerolog/log"
+)
+
+func getComplianceRequestImagePool(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "public_id")
+ if code == "" {
+ http.Error(w, "empty public_id", http.StatusBadRequest)
+ return
+ }
+
+ ctx := r.Context()
+ comp, err := models.ComplianceReportRequests.Query(
+ models.Preload.ComplianceReportRequest.Site(),
+ models.SelectWhere.ComplianceReportRequests.PublicID.EQ(code),
+ ).One(ctx, db.PGInstance.BobDB)
+ if err != nil {
+ http.Error(w, "no comp", http.StatusInternalServerError)
+ return
+ }
+
+ site := comp.R.Site
+ org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, site.OrganizationID)
+ if err != nil {
+ http.Error(w, "no org", http.StatusInternalServerError)
+ return
+ }
+ envelope, err := platform.ParcelEnvelope(ctx, site.ParcelID)
+ if err != nil {
+ log.Error().Err(err).Msg("parcel envelop failure")
+ http.Error(w, "parcel env", http.StatusInternalServerError)
+ return
+ }
+ log.Info().Int("len", len(*envelope)).Msg("got envelope")
+ level := uint(12)
+ ring := (*envelope)[0]
+ p := ring[0]
+ img, err := imagetile.ImageAtPoint(ctx, org, level, p[0], p[1])
+ w.Header().Set("Content-Type", "image/png")
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(img)))
+ _, err = io.Copy(w, bytes.NewBuffer(img))
+ if err != nil {
+ http.Error(w, "failed copy", http.StatusInternalServerError)
+ return
+ }
+}
diff --git a/html/template/sync/mailer.html b/html/template/sync/mailer.html
new file mode 100644
index 00000000..2f7d368e
--- /dev/null
+++ b/html/template/sync/mailer.html
@@ -0,0 +1,401 @@
+{{ template "sync/layout/base.html" . }}
+
+{{ define "title" }}Mailer{{ end }}
+{{ define "extraheader" }}
+
+{{ end }}
+{{ define "content" }}
+
+
+
+
+
+
+ Aerial review indicates
+ possible standing water at
+ this address.
+ Upload a photo to confirm
+ conditions and close this
+ case.
+ Not your property? Please
+ let us know.
+
+

+
+
id {{ .DocumentID }}
+
+
+
+
Dear Resident,
+
+ The {{ .Organization.Name }} is contacting you because recent aerial
+ imagery indicates that your swimming pool may be holding standing water.
+ When water sits without circulation or treatment, mosquitoes can develop
+ quickly and affect the surrounding neighborhood.
+
+
+ Action requested
+ Please scan the QR code on this letter and upload a current photo of
+ your pool. The image should clearly show the deep end and overall water
+ condition. We request a photo whether the water appears clear, cloudy,
+ or green so we can accurately assess the situation and close the matter
+ if no issue exists.
+
+
+ If your pool has started to look more like a pond than a pool, that is
+ more common than most people realize. Standing water creates ideal
+ conditions for mosquito production. If treatment is needed, we can
+ coordinate access while longer-term maintenance or repairs are
+ addressed.
+
+ Our objective is to prevent mosquito production and protect your
+ neighborhood. A quick photo response through the QR code is the fastest
+ way to resolve this.
+
+ If you are unable to use the QR code, please visit {{ .ReportURL }} or
+ contact our office for assistance.
+
+
+ Sincerely,
+ {{ .Organization.Name }}
+ {{ .Organization.OfficeAddressStreet.GetOr "" }} -
+ {{ .Organization.OfficeAddressCity.GetOr "" }},
+ {{ .Organization.OfficeAddressState.GetOr "" }}
+ {{ .Organization.OfficeAddressPostalCode.GetOr "" }}
+ Phone:
+ {{ .Organization.OfficePhone.GetOr "" }}
+
+

+
+
+
+
Scan. Snap. Done.
+

+
+
Takes less than 60 seconds.
+
+ - Scan the QR code
+ - Upload one pool
photo
+ - Case closed if clear
+
+ If treatment is needed, we
coordinate next steps.
+
+
Clear photo tips
+
+ - Show the full pool
+ - Deep end visible
+ - Take in daylight
+ - Avoid glare
+
+
+
+
+
+
+
+
+
+
+ La revisión aérea indica
+ posible agua estancada en
+ esta dirección.
+ Suba una foto para confirmar
+ las condiciones y cerrar este
+ caso.
+ ¿No es su propiedad? Por favor
+ infórmenos.
+
+

+
+
id {{ .DocumentID }}
+
+
+
+
Estimado Residente,
+
+ El Distrito de Control de Mosquitos y Vectores del Delta se comunica con
+ usted porque imágenes aéreas recientes indican que su piscina puede
+ estar reteniendo agua estancada. Cuando el agua permanece sin
+ circulación o tratamiento, los mosquitos pueden desarrollarse
+ rápidamente y afectar al vecindario.
+
+
+ Acción requerida
+ Por favor escanee el código QR en esta carta y suba una foto actual de
+ su piscina. La imagen debe mostrar claramente la parte profunda y la
+ condición general del agua. Solicitamos una foto ya sea que el agua esté
+ clara, turbia o verde para poder evaluar la situación y cerrar el caso
+ si no existe ningún problema.
+
+
+ Si su piscina ha comenzado a parecer más un estanque que una piscina, es
+ más común de lo que muchos creen. El agua estancada crea condiciones
+ ideales para la producción de mosquitos. Si se necesita tratamiento,
+ podemos coordinar el acceso mientras se realizan reparaciones o
+ mantenimiento a largo plazo.
+
+ Nuestro objetivo es prevenir la producción de mosquitos y proteger su
+ vecindario. Una respuesta rápida con una foto a través del código QR es
+ la manera más rápida de resolver esto.
+
+ Si no puede usar el código QR, visite {{ .ReportURL }} o comuníquese con
+ nuestra oficina para recibir ayuda.
+
+
+ Atentamente,
+ {{ .Organization.Name }}
+ {{ .Organization.OfficeAddressStreet.GetOr "" }} -
+ {{ .Organization.OfficeAddressCity.GetOr "" }},
+ {{ .Organization.OfficeAddressState.GetOr "" }}
+ {{ .Organization.OfficeAddressPostalCode.GetOr "" }}
+ Teléfono:
+ {{ .Organization.OfficePhone.GetOr "" }}
+
+

+
+
+
+
Escanee. Tome foto. Listo.
+

+
+
Toma menos de 60 segundos.
+
+ - Escanee el código QR
+ - Suba una foto de la
piscina
+ - Caso cerrado si está claro
+
+ Si se necesita tratamiento,
coordinamos los siguientes pasos.
+
+
Consejos para la foto
+
+ - Muestre toda la piscina
+ - Parte profunda visible
+ - Tome la foto de día
+ - Evite reflejos
+
+
+
+
+{{ end }}
diff --git a/rmo/mailer.go b/rmo/mailer.go
new file mode 100644
index 00000000..9e9a09e1
--- /dev/null
+++ b/rmo/mailer.go
@@ -0,0 +1,12 @@
+package rmo
+
+import (
+ "net/http"
+ //"github.com/Gleipnir-Technology/nidus-sync/config"
+)
+
+type contentMailer struct{}
+
+func getMailer(w http.ResponseWriter, r *http.Request) {
+
+}
diff --git a/sync/mailer.go b/sync/mailer.go
new file mode 100644
index 00000000..dd57bc0c
--- /dev/null
+++ b/sync/mailer.go
@@ -0,0 +1,60 @@
+package sync
+
+import (
+ "net/http"
+ //"strconv"
+
+ "github.com/Gleipnir-Technology/nidus-sync/config"
+ "github.com/Gleipnir-Technology/nidus-sync/db"
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+ "github.com/Gleipnir-Technology/nidus-sync/html"
+ //"github.com/google/uuid"
+ "github.com/go-chi/chi/v5"
+)
+
+type contentMailer struct {
+ Config contentConfig
+ DocumentID string
+ LogoURL string
+ Organization *models.Organization
+ PoolImageURL string
+ QRCodeURL string
+ ReportURL string
+}
+
+// func getMailer(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentMailer], *errorWithStatus) {
+func getMailer(w http.ResponseWriter, r *http.Request) {
+}
+func getMailerPreview(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "code")
+ if code == "" {
+ http.Error(w, "empty code", http.StatusBadRequest)
+ return
+ }
+
+ ctx := r.Context()
+ comp, err := models.ComplianceReportRequests.Query(
+ models.Preload.ComplianceReportRequest.Site(),
+ models.SelectWhere.ComplianceReportRequests.PublicID.EQ(code),
+ ).One(ctx, db.PGInstance.BobDB)
+
+ if err != nil {
+ http.Error(w, "no comp", http.StatusInternalServerError)
+ return
+ }
+ site := comp.R.Site
+ org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, site.OrganizationID)
+ if err != nil {
+ http.Error(w, "no comp", http.StatusInternalServerError)
+ return
+ }
+ html.RenderOrError(w, "sync/mailer.html", contentMailer{
+ Config: newContentConfig(),
+ DocumentID: "00000000-0000-0000-0000-000000000000",
+ LogoURL: config.MakeURLNidus("/api/district/%s/logo", org.Slug.GetOr("unset")),
+ Organization: org,
+ PoolImageURL: config.MakeURLNidus("/api/compliance-request/image/pool/%s", code),
+ QRCodeURL: config.MakeURLNidus("/qr-code/mailer/%s", code),
+ ReportURL: config.MakeURLReport("/mailer/%s", code),
+ })
+}
diff --git a/sync/parcel.go b/sync/parcel.go
new file mode 100644
index 00000000..d3329cca
--- /dev/null
+++ b/sync/parcel.go
@@ -0,0 +1,14 @@
+package sync
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/Gleipnir-Technology/nidus-sync/db/models"
+)
+
+type contentParcel struct{}
+
+func getParcel(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*response[contentParcel], *errorWithStatus) {
+ return newResponse("sync/parcel.html", contentParcel{}), nil
+}
diff --git a/sync/qr.go b/sync/qr.go
new file mode 100644
index 00000000..5d2807f8
--- /dev/null
+++ b/sync/qr.go
@@ -0,0 +1,82 @@
+package sync
+
+import (
+ "github.com/skip2/go-qrcode"
+ "net/http"
+ "strconv"
+
+ "github.com/Gleipnir-Technology/nidus-sync/config"
+ "github.com/go-chi/chi/v5"
+)
+
+func getQRCodeMailer(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "code")
+ if code == "" {
+ respondError(w, "There should always be a id", nil, http.StatusBadRequest)
+ }
+ content := config.MakeURLReport("/mailer/%s", code)
+ writeQRCode(w, r, content)
+}
+
+func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
+ code := chi.URLParam(r, "code")
+ if code == "" {
+ respondError(w, "There should always be a code", nil, http.StatusBadRequest)
+ }
+ content := config.MakeURLNidus("/report/%s", code)
+ writeQRCode(w, r, content)
+}
+func writeQRCode(w http.ResponseWriter, r *http.Request, content string) {
+ // Get optional size parameter (default to 256)
+ size := 256
+ if sizeStr := r.URL.Query().Get("size"); sizeStr != "" {
+ var err error
+ size, err = strconv.Atoi(sizeStr)
+ if err != nil {
+ http.Error(w, "Invalid 'size' parameter, must be an integer", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Get optional error correction level (default to Medium)
+ level := qrcode.Medium
+ if levelStr := r.URL.Query().Get("level"); levelStr != "" {
+ switch levelStr {
+ case "L", "l":
+ level = qrcode.Low
+ case "M", "m":
+ level = qrcode.Medium
+ case "Q", "q":
+ level = qrcode.High
+ case "H", "h":
+ level = qrcode.Highest
+ default:
+ respondError(w, "Invalid 'level' parameter, must be L, M, Q, or H", nil, http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Generate the QR code
+ var qr *qrcode.QRCode
+ var err error
+ qr, err = qrcode.New(content, level)
+ if err != nil {
+ respondError(w, "Error generating QR code", err, http.StatusInternalServerError)
+ return
+ }
+
+ // Set the appropriate content type
+ w.Header().Set("Content-Type", "image/png")
+
+ // Generate PNG and write directly to the response writer
+ png, err := qr.PNG(size)
+ if err != nil {
+ respondError(w, "Error encoding QR code to PNG", err, http.StatusInternalServerError)
+ return
+ }
+
+ _, err = w.Write(png)
+ if err != nil {
+ respondError(w, "Error writing response", err, http.StatusInternalServerError)
+ }
+}