dana-medical-site/cmd/server/main.go
Abdulkadyr Karagishiev 121f286016 feat(crm): integrate website leads and online booking with new CRM and 1C
- Add internal/crm client with CanonicalJSON and HMAC-SHA256 signing for POST /api/public/leads
- Add database migration 0026_crm_deals.sql with crm_deal_id and partial indexes
- Add async lead intake dispatch to new CRM in handlers_leads.go
- Support dual-mode booking confirmation (CRM_MODE=both/crm_only/bitrix_only) in booking_confirm.go
- Add LinkCRM endpoint to bridge_1c.py and onec.Client
- Update config and env examples with CRM_API_URL, CRM_LEAD_INTAKE_SECRET, CRM_MODE
2026-10-16 18:01:49 +03:00

143 lines
3.7 KiB
Go

package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"dana/internal/api"
"dana/internal/bitrix"
"dana/internal/config"
"dana/internal/crm"
"dana/internal/megafon"
"dana/internal/notify"
"dana/internal/onec"
"dana/internal/prodoctorov"
"dana/internal/sberhealth"
"dana/internal/storage"
)
func main() {
if err := run(); err != nil {
slog.Error("fatal", "err", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
level := slog.LevelInfo
if cfg.Debug {
level = slog.LevelDebug
}
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
pd, err := prodoctorov.New(cfg.ProdoctorovCookie, cfg.ProdoctorovLPU)
if err != nil {
return err
}
sh, err := sberhealth.New(cfg.SberHealthCookie, cfg.SberHealthClinic)
if err != nil {
return err
}
store, err := storage.New(ctx, cfg.DatabaseURL)
if err != nil {
return err
}
defer store.Close()
if err := store.Migrate(ctx); err != nil {
return err
}
// Первый администратор заводится один раз, если таблица пуста.
if err := store.EnsureAdmin(ctx, cfg.AdminEmail, cfg.AdminPassword); err != nil {
return err
}
if err := api.SetTrustedProxies(cfg.TrustedProxies); err != nil {
return err
}
handler := api.New(store, cfg.MediaDir)
if err := handler.FailInterruptedReports(ctx); err != nil {
slog.Warn("mark interrupted reports", "err", err)
}
handler.WithPBX(megafon.New(cfg.MegafonURL, cfg.MegafonKey))
if cfg.OchevidnoMegafonURL != "" && cfg.OchevidnoMegafonKey != "" {
handler.WithClinicPBX("ochevidno", megafon.New(cfg.OchevidnoMegafonURL, cfg.OchevidnoMegafonKey))
}
go handler.RunPBXSync(ctx)
handler.WithOneC(onec.New(cfg.OnecURL, cfg.OnecKey, cfg.OnecAuthHeader))
handler.WithOneCWebhook(cfg.OnecWebhookKey)
handler.WithOneCTuning(time.Duration(cfg.OnecSyncSecs)*time.Second, cfg.OnecLongPoll)
go handler.RunOneCSync(ctx)
handler.WithBitrix(bitrix.New(cfg.BitrixWebhookURL))
handler.WithCRM(crm.New(cfg.CrmAPIURL, cfg.CrmLeadIntakeSecret), cfg.CrmMode)
go handler.RunBookingConfirm(ctx)
handler.WithSMSC(cfg.SMSCLogin, cfg.SMSCPassword)
handler.WithSMSCCallbackToken(cfg.SMSCCallbackToken)
handler.WithProdoctorov(pd)
handler.WithSberHealth(sh)
if err := handler.WithWebsite(cfg.WebDir, cfg.SiteURL); err != nil {
return err
}
if err := handler.WithSSR(cfg.SSRRenderer, cfg.NodeBinary); err != nil {
return err
}
if cfg.SMTPHost != "" {
handler.WithNotifier(&notify.Email{
Host: cfg.SMTPHost,
Port: cfg.SMTPPort,
User: cfg.SMTPUser,
Password: cfg.SMTPPassword,
From: cfg.NotifyFrom,
To: cfg.NotifyTo,
SiteURL: cfg.SiteURL,
})
slog.Info("lead notifications by email", "smtp", cfg.SMTPHost, "to", cfg.NotifyTo)
} else {
slog.Warn("SMTP_HOST is not set: lead notifications are disabled")
}
srv := &http.Server{
Addr: cfg.Addr,
Handler: handler.Routes(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() {
slog.Info("listening", "addr", cfg.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
slog.Info("shutting down")
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
return srv.Shutdown(shutdownCtx)
}