auto mail generation

This commit is contained in:
2026-09-29 16:53:21 +05:30
parent b18080d429
commit b46902f51b
27 changed files with 2765 additions and 80 deletions

View File

@@ -3,6 +3,7 @@ package controllers
import (
"fmt"
"log"
"nearle/middleware"
"nearle/models"
"nearle/services"
"net/http"
@@ -346,7 +347,8 @@ func (ctl *TenantController) CreateStaff(c *fiber.Ctx) error {
})
}
if err := ctl.tenantService.CreateStaff(data); err != nil {
invite, err := ctl.tenantService.CreateStaff(data)
if err != nil {
// A rejected PIN, a missing name, a role nobody set — these are things
// the person filling in the form can fix, so they come back as 400 with
// the reason. This answered 500 with a body claiming 409, which told a
@@ -358,10 +360,17 @@ func (ctl *TenantController) CreateStaff(c *fiber.Ctx) error {
})
}
// The person was hired either way. Whether they were emailed their
// first-password link is reported beside that rather than folded into
// `status`: this account is created with no password and the link is the only
// way in, so an operator who is not told cannot know they have added somebody
// who cannot sign in.
return c.JSON(fiber.Map{
"code": http.StatusCreated,
"message": "Staff created successfully",
"status": true,
"code": http.StatusCreated,
"message": "Staff created successfully",
"status": true,
"invited": invite.Sent,
"invitereason": invite.Reason,
})
}
@@ -436,7 +445,7 @@ func (ctl *TenantController) CreateTenantUser(c *fiber.Ctx) error {
})
}
result, err := ctl.tenantService.CreateTenantUser(data)
result, invite, err := ctl.tenantService.CreateTenantUser(data)
if err != nil {
if err.Error() == "Tenant Already Exists" {
return c.Status(http.StatusConflict).JSON(fiber.Map{
@@ -453,11 +462,21 @@ func (ctl *TenantController) CreateTenantUser(c *fiber.Ctx) error {
})
}
// The tenant was created either way. The invitation is reported beside it
// rather than folded into `status`, because a merchant who exists and has
// not been emailed is a task for the operator — resend, or correct the
// address — and not a failed onboarding to be retried.
//
// `invited: false` with a reason is the state the platform console shows on
// the tenant, so it never has to guess whether the email went.
return c.Status(http.StatusCreated).JSON(fiber.Map{
"code": 201,
"status": true,
"message": "Successfully Created",
"details": result,
"invited": invite.Sent,
// Omitted when it sent, so a successful onboarding carries no apology.
"invitereason": invite.Reason,
})
}
@@ -776,3 +795,79 @@ func (ctl *TenantController) AssignPartner(c *fiber.Ctx) error {
"code": http.StatusOK, "status": true, "message": "Successfully Updated",
})
}
// ResendInvite re-issues a merchant's first-password link.
//
// ── Why this is platform staff only ─────────────────────────────────────────
//
// It mints a credential. `middleware.WebAuth` already pins a merchant's session
// to their own tenant, so a shop could at most re-invite itself — but the
// account it would be inviting is the one signing in to ask, which can only
// happen if that account already has a password, and the service refuses that
// case outright.
//
// So the only caller this is for is Nearle's own staff, chasing a merchant who
// never received the mail. Saying so explicitly is better than relying on two
// other checks to make the wrong case impossible.
func (ctl *TenantController) ResendInvite(c *fiber.Ctx) error {
claims, ok := middleware.WebClaimsFrom(c)
if !ok || !claims.IsPlatformAccount() {
return c.Status(http.StatusForbidden).JSON(fiber.Map{
"code": http.StatusForbidden, "status": false,
"message": "Only Nearle staff can resend an invitation.",
})
}
// Either a tenant — meaning its owner, the one account onboarding created —
// or one named person. Staff added later and the login every branch spawns
// are created with no password too, and a business has many of them, so
// "the tenant's invitation" cannot reach them.
var req struct {
Tenantid int `json:"tenantid"`
Userid int `json:"userid"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "status": false, "message": "Invalid request body",
})
}
if req.Tenantid <= 0 && req.Userid <= 0 {
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
"code": http.StatusBadRequest, "status": false,
"message": "Send a tenantid to re-invite the owner, or a userid to re-invite one person.",
})
}
// `userid` wins when both arrive. It is the more specific of the two, and a
// caller that sent a person's id meant that person — silently emailing the
// owner instead would be the wrong mailbox with no sign anything was off.
var (
outcome services.InviteOutcome
err error
)
if req.Userid > 0 {
outcome, err = ctl.tenantService.ResendInviteToUser(req.Userid)
} else {
outcome, err = ctl.tenantService.ResendInvite(req.Tenantid)
}
if err != nil {
// 409, not 500. Every failure here is a business fact the operator can
// act on — no such tenant, an address that matches no login, a merchant
// already set up — rather than a fault in the server.
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict, "status": false, "message": err.Error(),
})
}
if !outcome.Sent {
// The tenant is fine and the mail did not go. Reported as a failure
// because the operator pressed a button expecting an email to leave,
// and the reason names what to fix.
return c.Status(http.StatusConflict).JSON(fiber.Map{
"code": http.StatusConflict, "status": false, "message": outcome.Reason,
})
}
return c.JSON(fiber.Map{
"code": http.StatusOK, "status": true, "message": "Invitation sent.",
})
}