aravind changes

This commit is contained in:
2026-08-25 16:37:05 +05:30
parent cadea4bd92
commit b6f8655909
27 changed files with 5058 additions and 163 deletions

View File

@@ -209,7 +209,7 @@ func (s *WorkflowService) Hire(ctx context.Context, ident authctx.Identity,
// happened without a log entry, or a log entry for a hire that rolled
// back, are both worse than neither.
if _, err := repo.New(activity, tx).Insert(ctx, ident, domain.Record{
"event_type": "candidate_hired",
"event_type": "hire_candidate",
"details": fmt.Sprintf("%s was hired", name),
"application_id": applicationID,
"position_id": app["job_posting_id"],
@@ -230,6 +230,11 @@ func (s *WorkflowService) Hire(ctx context.Context, ident authctx.Identity,
/* ── Assign ─────────────────────────────────────────────────────────────── */
// AssignWorker is one worker in an assign request.
//
// Three ways of naming the application this placement belongs to, in order of
// precedence: an id the caller already has, an `application` payload to find or
// file one from, and neither — a worker placed straight from the talent pool,
// who has no application and is not given an invented one.
type AssignWorker struct {
WorkerEmail string `json:"worker_email"`
WorkerName string `json:"worker_name"`
@@ -239,6 +244,17 @@ type AssignWorker struct {
WorkerProfileID *string `json:"worker_profile_id"`
MatchScore *int `json:"match_score"`
Source string `json:"source"`
// Application is the application to attach this worker to when no
// ApplicationID is supplied: the same fields POST /job-applications takes.
//
// It exists because an application is what puts a person in the pipeline
// for a role, and every downstream step keys on it — the candidate record
// is addressed by it, and an AI interview takes one as its subject. A
// worker assigned without one is unreachable: nothing to open, and nobody
// to interview. The frontend was creating it in a second, untransacted
// request; this carries it into the same transaction as the assignment.
Application *domain.Record `json:"application"`
}
// AssignRequest is the body of POST /job-postings/{id}/assignments.
@@ -258,6 +274,11 @@ type AssignResult struct {
// request and one transaction. All-or-nothing across the whole batch: assigning
// six workers and having the fourth fail should not leave three assigned, three
// not, and the caller unsure which.
//
// Per worker the flow is: settle the application (see settleApplication —
// patch the one named, find-or-file the one described, or neither), insert the
// assignment linked to whatever that produced, and write the audit entry. The
// application comes first because the assignment references it.
func (s *WorkflowService) Assign(ctx context.Context, ident authctx.Identity,
postingID string, req AssignRequest) (*AssignResult, error) {
@@ -324,22 +345,43 @@ func (s *WorkflowService) Assign(ctx context.Context, ident authctx.Identity,
assignRepo := repo.New(assignments, tx)
appRepo := repo.New(apps, tx)
// The application is filed through the service rather than the
// repository so that a payload the caller sent is validated exactly as
// POST /job-applications would validate it — unknown fields rejected,
// enums checked, required fields demanded — instead of reaching SQL and
// coming back as a constraint violation.
appSvc := New(apps, tx)
activityRepo := repo.New(activity, tx)
for i, w := range req.Workers {
// The application is settled BEFORE the assignment is written,
// because the assignment carries the reference to it and a row
// cannot point at one that does not exist yet. An empty id means
// this worker legitimately has no application.
applicationID, err := s.settleApplication(ctx, ident, appRepo, appSvc, postingID, w)
if err != nil {
return annotate(err, i)
}
record := domain.Record{
"job_posting_id": postingID,
"worker_email": w.WorkerEmail,
"worker_name": w.WorkerName,
"starts_at": w.StartsAt,
"status": "active",
"source": orDefault(w.Source, "manual"),
// `owliver` is the column's own default (000001:511) and what
// the frontend sends. Substituting `manual` here made the API
// and the schema disagree about what an unspecified source
// means, so a row written through this endpoint and a row
// written through POST /assignments recorded different origins
// for the same action.
"source": orDefault(w.Source, "owliver"),
}
if w.EndsAt != nil {
record["ends_at"] = *w.EndsAt
}
if w.ApplicationID != nil {
record["application_id"] = *w.ApplicationID
if applicationID != "" {
record["application_id"] = applicationID
}
if w.WorkerProfileID != nil {
record["worker_profile_id"] = *w.WorkerProfileID
@@ -354,29 +396,14 @@ func (s *WorkflowService) Assign(ctx context.Context, ident authctx.Identity,
}
out.Assignments = append(out.Assignments, created)
// The application moves to `assigned` only when one was named. A
// worker can be placed without having applied — that is what the
// talent pool is for — and inventing an application for them would
// be worse than leaving the link absent.
if w.ApplicationID != nil {
updated, err := appRepo.Update(ctx, ident, *w.ApplicationID,
domain.Record{"status": "assigned"})
if err != nil {
return annotate(err, i)
}
if updated == nil {
return domain.NotFound("JobApplication", *w.ApplicationID)
}
}
entry := domain.Record{
"event_type": "worker_assigned",
"event_type": "assign_employee",
"details": fmt.Sprintf("%s was assigned", orDefault(w.WorkerName, w.WorkerEmail)),
"position_id": postingID,
"worker_email": w.WorkerEmail,
}
if w.ApplicationID != nil {
entry["application_id"] = *w.ApplicationID
if applicationID != "" {
entry["application_id"] = applicationID
}
if _, err := activityRepo.Insert(ctx, ident, entry); err != nil {
return annotate(err, i)
@@ -391,6 +418,122 @@ func (s *WorkflowService) Assign(ctx context.Context, ident authctx.Identity,
return out, nil
}
// settleApplication resolves the application an assignment belongs to and
// returns its id, or "" when this worker has none.
//
// Three cases, and the middle one is why this exists:
//
// - an id was supplied — move that application to `assigned`. Unchanged
// behaviour, and it still wins over any payload, because an id is a
// decision the caller has already made.
// - an `application` payload was supplied — find the application on this
// posting for this email, and move it to `assigned` if there is one or file
// it if there is not. (job_posting_id, email) is UNIQUE
// (job_applications_posting_email_key, 000001), so there is at most one to
// find and the insert cannot produce a second.
// - neither — nothing. A worker placed straight from the talent pool has no
// application, and inventing one for them would be worse than leaving the
// link absent.
//
// Everything here runs inside the caller's transaction, which is what makes the
// find-or-create safe: a concurrent assign of the same person to the same
// posting blocks on the unique index and is then reported as a conflict, rather
// than racing past the lookup and writing a duplicate.
func (s *WorkflowService) settleApplication(ctx context.Context, ident authctx.Identity,
appRepo *repo.Repo, appSvc *Service, postingID string, w AssignWorker) (string, error) {
if w.ApplicationID != nil {
updated, err := appRepo.Update(ctx, ident, *w.ApplicationID,
domain.Record{"status": "assigned"})
if err != nil {
return "", err
}
if updated == nil {
return "", domain.NotFound("JobApplication", *w.ApplicationID)
}
return *w.ApplicationID, nil
}
if w.Application == nil {
return "", nil
}
existing, err := findApplication(ctx, ident, appRepo, postingID, w.WorkerEmail)
if err != nil {
return "", err
}
if existing != nil {
id, _ := existing["id"].(string)
updated, err := appRepo.Update(ctx, ident, id, domain.Record{"status": "assigned"})
if err != nil {
return "", err
}
if updated == nil {
return "", domain.NotFound("JobApplication", id)
}
return id, nil
}
// The posting and the person come from the assignment, not from the
// payload. A body naming a different posting or a different email would
// file an application about somebody other than the worker being placed,
// and the lookup above would never find it again.
record := domain.Record{}
for k, v := range *w.Application {
record[k] = v
}
record["job_posting_id"] = postingID
record["email"] = w.WorkerEmail
if _, ok := record["applicant_name"]; !ok {
record["applicant_name"] = w.WorkerName
}
if _, ok := record["status"]; !ok {
record["status"] = "assigned"
}
created, err := appSvc.Create(ctx, ident, record)
if err != nil {
return "", err
}
id, _ := created["id"].(string)
return id, nil
}
// findApplication returns this posting's application for this email, or nil.
//
// The read goes through the repository so it carries the same organization
// scope and ownership predicate every other read does — an application in
// another tenant is not found, rather than found and then refused. The email
// column is citext, so the comparison is case-insensitive: the same equality
// the frontend performed with toLowerCase before it had this endpoint.
func findApplication(ctx context.Context, ident authctx.Identity, appRepo *repo.Repo,
postingID, email string) (domain.Record, error) {
res := appRepo.Resource()
postingCol, ok := res.Column("job_posting_id")
if !ok {
return nil, domain.Internal(errors.New("service: job_applications has no job_posting_id column"))
}
emailCol, ok := res.Column("email")
if !ok {
return nil, domain.Internal(errors.New("service: job_applications has no email column"))
}
page, err := appRepo.List(ctx, ident, domain.ListParams{
Limit: 1,
Filters: []domain.Filter{
{Column: postingCol, Values: []string{postingID}},
{Column: emailCol, Values: []string{email}},
},
})
if err != nil {
return nil, err
}
if len(page.Records) == 0 {
return nil, nil
}
return page.Records[0], nil
}
/* ── Helpers ────────────────────────────────────────────────────────────── */
// pick takes the caller's value for a key when they supplied a usable one, and