Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.escalated.controllers.widget;

import dev.escalated.models.Reply;
import dev.escalated.dto.GuestReplyDto;
import dev.escalated.dto.GuestTicketDto;
import dev.escalated.models.Ticket;
import dev.escalated.services.TicketService;
import java.util.List;
Expand All @@ -24,25 +25,24 @@ public GuestAccessController(TicketService ticketService) {
}

@GetMapping("/tickets/{token}")
public ResponseEntity<Ticket> show(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findByGuestToken(token));
public ResponseEntity<GuestTicketDto> show(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findGuestView(token));
}

@GetMapping("/tickets/{token}/replies")
public ResponseEntity<List<Reply>> replies(@PathVariable String token) {
Ticket ticket = ticketService.findByGuestToken(token);
return ResponseEntity.ok(ticketService.getReplies(ticket.getId()));
public ResponseEntity<List<GuestReplyDto>> replies(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findGuestReplies(token));
}

@PostMapping("/tickets/{token}/replies")
// Shares the widget reply counter; counted before the token lookup.
@GuestThrottle(GuestThrottle.Scope.REPLY)
public ResponseEntity<Reply> addReply(@PathVariable String token, @RequestBody Map<String, String> body) {
public ResponseEntity<GuestReplyDto> addReply(@PathVariable String token, @RequestBody Map<String, String> body) {
Ticket ticket = ticketService.findByGuestToken(token);
// The guest token belongs to the requester, so the reply is theirs;
// any name or email in the body is ignored (as in the NestJS reference).
return ResponseEntity.status(201).body(ticketService.addReply(
return ResponseEntity.status(201).body(GuestReplyDto.from(ticketService.addReply(
ticket.getId(), body.get("body"), ticket.getRequesterName(), ticket.getRequesterEmail(),
"customer", false));
"customer", false)));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.escalated.controllers.widget;

import dev.escalated.dto.GuestReplyDto;
import dev.escalated.dto.GuestTicketDto;
import dev.escalated.models.KnowledgeBaseArticle;
import dev.escalated.models.Reply;
import dev.escalated.models.Ticket;
Expand Down Expand Up @@ -50,26 +52,25 @@ public ResponseEntity<Ticket> createTicket(@RequestBody Map<String, String> body
}

@GetMapping("/tickets/{token}")
public ResponseEntity<Ticket> getTicketByToken(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findByGuestToken(token));
public ResponseEntity<GuestTicketDto> getTicketByToken(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findGuestView(token));
}

@PostMapping("/tickets/{token}/replies")
// Counted before the token lookup, so wrong-token guesses count too.
@GuestThrottle(GuestThrottle.Scope.REPLY)
public ResponseEntity<Reply> addReply(@PathVariable String token, @RequestBody Map<String, String> body) {
public ResponseEntity<GuestReplyDto> addReply(@PathVariable String token, @RequestBody Map<String, String> body) {
Ticket ticket = ticketService.findByGuestToken(token);
// The guest token belongs to the requester, so the reply is theirs;
// any name or email in the body is ignored (as in the NestJS reference).
Reply reply = ticketService.addReply(ticket.getId(),
body.get("body"), ticket.getRequesterName(), ticket.getRequesterEmail(), "customer", false);
return ResponseEntity.status(201).body(reply);
return ResponseEntity.status(201).body(GuestReplyDto.from(reply));
}

@GetMapping("/tickets/{token}/replies")
public ResponseEntity<List<Reply>> getReplies(@PathVariable String token) {
Ticket ticket = ticketService.findByGuestToken(token);
return ResponseEntity.ok(ticketService.getReplies(ticket.getId()));
public ResponseEntity<List<GuestReplyDto>> getReplies(@PathVariable String token) {
return ResponseEntity.ok(ticketService.findGuestReplies(token));
}

@GetMapping("/kb/search")
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/dev/escalated/dto/GuestReplyDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.escalated.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import dev.escalated.models.Attachment;
import dev.escalated.models.Reply;
import java.time.Instant;
import java.util.List;

/**
* A reply as a guest sees it: the message and who wrote it, never the
* author's email or the ticket it belongs to. Only public replies are ever
* turned into one; internal notes stay with staff.
*/
public record GuestReplyDto(
Long id,
String body,
@JsonProperty("author_name") String authorName,
@JsonProperty("author_type") String authorType,
@JsonProperty("is_agent") boolean isAgent,
@JsonProperty("created_at") Instant createdAt,
List<Attachment> attachments) {

public static GuestReplyDto from(Reply reply) {
return new GuestReplyDto(
reply.getId(),
reply.getBody(),
reply.getAuthorName(),
reply.getAuthorType(),
"agent".equals(reply.getAuthorType()),
reply.getCreatedAt(),
List.copyOf(reply.getAttachments()));
}
}
71 changes: 71 additions & 0 deletions src/main/java/dev/escalated/dto/GuestTicketDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dev.escalated.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import dev.escalated.models.Attachment;
import dev.escalated.models.Reply;
import dev.escalated.models.Ticket;
import dev.escalated.models.TicketPriority;
import dev.escalated.models.TicketStatus;
import java.time.Instant;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
* A ticket as the guest holding its access token sees it: the fields the
* shared frontend's guest ticket page reads (reference, subject, description,
* status, priority, department name, attachments, replies) and nothing staff
* keep to themselves. Internal notes, the activity log, the assigned agent,
* SLA data, tags, links and side conversations are left out.
*/
public record GuestTicketDto(
Long id,
String reference,
String subject,
String description,
TicketStatus status,
TicketPriority priority,
String channel,
Map<String, String> department,
@JsonProperty("requester_name") String requesterName,
@JsonProperty("created_at") Instant createdAt,
@JsonProperty("updated_at") Instant updatedAt,
@JsonProperty("resolved_at") Instant resolvedAt,
@JsonProperty("closed_at") Instant closedAt,
List<Attachment> attachments,
List<GuestReplyDto> replies) {

public static GuestTicketDto from(Ticket ticket) {
List<GuestReplyDto> publicReplies = ticket.getReplies().stream()
.filter(reply -> !reply.isInternal())
.sorted(Comparator.comparing(Reply::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())))
.map(GuestReplyDto::from)
.toList();

// Attachments sent with a reply travel with that reply; only the ones
// filed on the ticket itself belong here.
List<Attachment> ticketAttachments = ticket.getAttachments().stream()
.filter(attachment -> attachment.getReply() == null)
.toList();

return new GuestTicketDto(
ticket.getId(),
ticket.getTicketNumber(),
ticket.getSubject(),
ticket.getBody(),
ticket.getStatus(),
ticket.getPriority(),
ticket.getChannel(),
ticket.getDepartment() == null
? null
: Collections.singletonMap("name", ticket.getDepartment().getName()),
ticket.getRequesterName(),
ticket.getCreatedAt(),
ticket.getUpdatedAt(),
ticket.getResolvedAt(),
ticket.getClosedAt(),
ticketAttachments,
publicReplies);
}
}
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/AgentProfile.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -69,6 +70,7 @@ public class AgentProfile extends BaseEntity {
@OneToOne(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true)
private AgentCapacity capacity;

@JsonIgnore
@OneToMany(mappedBy = "assignedAgent")
private List<Ticket> assignedTickets = new ArrayList<>();

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/CustomFieldValue.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +12,7 @@
@Table(name = "escalated_custom_field_values")
public class CustomFieldValue extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dev/escalated/models/Department.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
Expand All @@ -25,9 +26,11 @@ public class Department extends BaseEntity {
@Column(name = "sort_order")
private int sortOrder = 0;

@JsonIgnore
@OneToMany(mappedBy = "department")
private List<Ticket> tickets = new ArrayList<>();

@JsonIgnore
@OneToMany(mappedBy = "department")
private List<AgentProfile> agents = new ArrayList<>();

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/EscalationRule.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +12,7 @@
@Table(name = "escalated_escalation_rules")
public class EscalationRule extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sla_policy_id", nullable = false)
private SlaPolicy slaPolicy;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/Reply.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -15,6 +16,7 @@
@Table(name = "escalated_replies")
public class Reply extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/SatisfactionRating.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +12,7 @@
@Table(name = "escalated_satisfaction_ratings")
public class SatisfactionRating extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/SideConversation.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -15,6 +16,7 @@
@Table(name = "escalated_side_conversations")
public class SideConversation extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/SideConversationReply.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,6 +12,7 @@
@Table(name = "escalated_side_conversation_replies")
public class SideConversationReply extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "side_conversation_id", nullable = false)
private SideConversation sideConversation;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/Tag.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToMany;
Expand All @@ -19,6 +20,7 @@ public class Tag extends BaseEntity {
@Column
private String color;

@JsonIgnore
@ManyToMany(mappedBy = "tags")
private Set<Ticket> tickets = new HashSet<>();

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/TicketActivity.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand All @@ -14,6 +15,7 @@
@Table(name = "escalated_ticket_activities")
public class TicketActivity extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/dev/escalated/models/TicketLink.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -11,10 +12,12 @@
@Table(name = "escalated_ticket_links")
public class TicketLink extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "source_ticket_id", nullable = false)
private Ticket sourceTicket;

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "target_ticket_id", nullable = false)
private Ticket targetTicket;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/escalated/models/TicketSubjectLink.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.escalated.models;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -20,6 +21,7 @@
columnList = "subject_type, subject_id"))
public class TicketSubjectLink extends BaseEntity {

@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ticket_id", nullable = false)
private Ticket ticket;
Expand Down
Loading
Loading