Compare commits
1 Commits
fix/temp-d
...
fix/fix-re
| Author | SHA1 | Date | |
|---|---|---|---|
| 576b3f4757 |
@@ -114,7 +114,7 @@ func (h *OrderHandler) CreateRefundRequest(w http.ResponseWriter, r *http.Reques
|
|||||||
text += "\n\n" + quoted
|
text += "\n\n" + quoted
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := notify.SlackPostToOrderThread(order.ID, text); err != nil {
|
if err := notify.SlackPostToOrderThreadBroadcast(order.ID, text); err != nil {
|
||||||
utils.RespondError(w, http.StatusBadGateway, "SLACK_ERROR", "failed to send refund request", nil)
|
utils.RespondError(w, http.StatusBadGateway, "SLACK_ERROR", "failed to send refund request", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,27 @@ const (
|
|||||||
DisplayKindBrand DisplayKind = "brand" // shipped / in transit
|
DisplayKindBrand DisplayKind = "brand" // shipped / in transit
|
||||||
DisplayKindSuccess DisplayKind = "success" // delivered
|
DisplayKindSuccess DisplayKind = "success" // delivered
|
||||||
DisplayKindError DisplayKind = "error" // cancelled, failed, refunded
|
DisplayKindError DisplayKind = "error" // cancelled, failed, refunded
|
||||||
DisplayKindWarning DisplayKind = "warning" // refund pending, delivery issue
|
DisplayKindWarning DisplayKind = "warning" // delivery issue, returned
|
||||||
|
DisplayKindRefund DisplayKind = "refund" // refund requested, awaiting review
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// refundPending reports whether the order has an open customer refund request
|
||||||
|
// that has not yet been resolved by a terminal status change.
|
||||||
|
func (order *Order) refundPending() bool {
|
||||||
|
if order.LastRefundRequestAt == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch order.Status {
|
||||||
|
case OrderStatusRefunded, OrderStatusCancelled, OrderStatusFailed:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (order *Order) DisplayState() string {
|
func (order *Order) DisplayState() string {
|
||||||
|
if order.refundPending() {
|
||||||
|
return "Refund Requested"
|
||||||
|
}
|
||||||
switch order.Status {
|
switch order.Status {
|
||||||
case OrderStatusPending:
|
case OrderStatusPending:
|
||||||
return "Awaiting payment"
|
return "Awaiting payment"
|
||||||
@@ -54,6 +71,9 @@ func (order *Order) DisplayState() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (order *Order) DisplayKind() DisplayKind {
|
func (order *Order) DisplayKind() DisplayKind {
|
||||||
|
if order.refundPending() {
|
||||||
|
return DisplayKindRefund
|
||||||
|
}
|
||||||
switch order.Status {
|
switch order.Status {
|
||||||
case OrderStatusPending:
|
case OrderStatusPending:
|
||||||
return DisplayKindNeutral
|
return DisplayKindNeutral
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestOrderDisplayState(t *testing.T) {
|
func TestOrderDisplayState(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
order Order
|
order Order
|
||||||
@@ -23,6 +27,10 @@ func TestOrderDisplayState(t *testing.T) {
|
|||||||
{"cancelled", Order{Status: OrderStatusCancelled}, "Cancelled", DisplayKindError},
|
{"cancelled", Order{Status: OrderStatusCancelled}, "Cancelled", DisplayKindError},
|
||||||
{"refunded", Order{Status: OrderStatusRefunded}, "Refunded", DisplayKindError},
|
{"refunded", Order{Status: OrderStatusRefunded}, "Refunded", DisplayKindError},
|
||||||
{"failed", Order{Status: OrderStatusFailed}, "Payment failed", DisplayKindError},
|
{"failed", Order{Status: OrderStatusFailed}, "Payment failed", DisplayKindError},
|
||||||
|
{"paid with open refund request", Order{Status: OrderStatusPaid, LastRefundRequestAt: &now}, "Refund Requested", DisplayKindRefund},
|
||||||
|
{"delivered with open refund request", Order{Status: OrderStatusDelivered, LastRefundRequestAt: &now}, "Refund Requested", DisplayKindRefund},
|
||||||
|
{"refunded order ignores prior request", Order{Status: OrderStatusRefunded, LastRefundRequestAt: &now}, "Refunded", DisplayKindError},
|
||||||
|
{"cancelled order ignores prior request", Order{Status: OrderStatusCancelled, LastRefundRequestAt: &now}, "Cancelled", DisplayKindError},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -191,6 +191,17 @@ func SlackUnpinOrder(orderID uint) {
|
|||||||
// It looks up the thread ts from the database and returns an error when the
|
// It looks up the thread ts from the database and returns an error when the
|
||||||
// reply cannot be posted.
|
// reply cannot be posted.
|
||||||
func SlackPostToOrderThread(orderID uint, text string) error {
|
func SlackPostToOrderThread(orderID uint, text string) error {
|
||||||
|
return slackPostToOrderThread(orderID, text, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SlackPostToOrderThreadBroadcast posts a thread reply that is also surfaced
|
||||||
|
// to the channel (reply_broadcast=true) so channel members get a normal
|
||||||
|
// notification. Use for messages that need immediate operator attention.
|
||||||
|
func SlackPostToOrderThreadBroadcast(orderID uint, text string) error {
|
||||||
|
return slackPostToOrderThread(orderID, text, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func slackPostToOrderThread(orderID uint, text string, broadcast bool) error {
|
||||||
token := os.Getenv("SLACK_BOT_TOKEN")
|
token := os.Getenv("SLACK_BOT_TOKEN")
|
||||||
channel := os.Getenv("SLACK_CHANNEL")
|
channel := os.Getenv("SLACK_CHANNEL")
|
||||||
if token == "" || channel == "" {
|
if token == "" || channel == "" {
|
||||||
@@ -206,11 +217,16 @@ func SlackPostToOrderThread(orderID uint, text string) error {
|
|||||||
return fmt.Errorf("order has no slack thread")
|
return fmt.Errorf("order has no slack thread")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := postSlackMessage(token, map[string]any{
|
payload := map[string]any{
|
||||||
"channel": channel,
|
"channel": channel,
|
||||||
"thread_ts": *order.SlackThreadTS,
|
"thread_ts": *order.SlackThreadTS,
|
||||||
"text": text,
|
"text": text,
|
||||||
}); !ok {
|
}
|
||||||
|
if broadcast {
|
||||||
|
payload["reply_broadcast"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := postSlackMessage(token, payload); !ok {
|
||||||
slog.Warn("failed to post slack thread reply", "order_id", orderID)
|
slog.Warn("failed to post slack thread reply", "order_id", orderID)
|
||||||
return fmt.Errorf("failed to post slack thread reply")
|
return fmt.Errorf("failed to post slack thread reply")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,10 +278,9 @@ func (m Model) displayStyle(k models.DisplayKind) lipgloss.Style {
|
|||||||
case models.DisplayKindError:
|
case models.DisplayKindError:
|
||||||
return m.theme.TextError()
|
return m.theme.TextError()
|
||||||
case models.DisplayKindWarning:
|
case models.DisplayKindWarning:
|
||||||
// !TODO add refunds and change this
|
|
||||||
// No warning color in theme yet, reuse loading pink as visual flag
|
|
||||||
// for now
|
|
||||||
return m.theme.TextLoading()
|
return m.theme.TextLoading()
|
||||||
|
case models.DisplayKindRefund:
|
||||||
|
return m.theme.TextBrand()
|
||||||
}
|
}
|
||||||
return m.theme.TextHighlight()
|
return m.theme.TextHighlight()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,10 +164,8 @@ func (m Model) newRefundTextarea() textarea.Model {
|
|||||||
ta.Placeholder = "Describe the issue (optional)..."
|
ta.Placeholder = "Describe the issue (optional)..."
|
||||||
ta.ShowLineNumbers = true
|
ta.ShowLineNumbers = true
|
||||||
ta.CharLimit = 65536
|
ta.CharLimit = 65536
|
||||||
ta.DynamicHeight = true
|
ta.DynamicHeight = false
|
||||||
ta.MinHeight = refundMinTextareaRows
|
ta.SetHeight(m.refundTextareaHeight())
|
||||||
ta.MaxHeight = max(refundMinTextareaRows, min(refundMaxTextareaRows, m.heightContainer-refundModalChrome))
|
|
||||||
ta.SetHeight(refundMinTextareaRows)
|
|
||||||
ta.SetWidth(m.refundTextareaWidth())
|
ta.SetWidth(m.refundTextareaWidth())
|
||||||
|
|
||||||
base := lipgloss.NewStyle()
|
base := lipgloss.NewStyle()
|
||||||
@@ -200,8 +198,7 @@ func (m Model) newRefundTextarea() textarea.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) resizeRefundTextarea(state *refundState) {
|
func (m Model) resizeRefundTextarea(state *refundState) {
|
||||||
state.textarea.MinHeight = refundMinTextareaRows
|
state.textarea.SetHeight(m.refundTextareaHeight())
|
||||||
state.textarea.MaxHeight = max(refundMinTextareaRows, min(refundMaxTextareaRows, m.heightContainer-refundModalChrome))
|
|
||||||
state.textarea.SetWidth(m.refundTextareaWidth())
|
state.textarea.SetWidth(m.refundTextareaWidth())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,22 +320,26 @@ func (m Model) renderRefundTextarea(state *refundState) string {
|
|||||||
border = m.theme.Highlight()
|
border = m.theme.Highlight()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wrapper := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(border)
|
||||||
|
|
||||||
|
contentWidth := m.refundContentWidth()
|
||||||
|
state.textarea.SetWidth(contentWidth - wrapper.GetHorizontalFrameSize())
|
||||||
|
state.textarea.SetHeight(m.refundTextareaHeight())
|
||||||
|
|
||||||
|
textareaBox := wrapper.Render(state.textarea.View())
|
||||||
|
|
||||||
counter := m.theme.TextDim().
|
counter := m.theme.TextDim().
|
||||||
Width(m.refundTextareaWidth()).
|
Width(contentWidth).
|
||||||
Align(lipgloss.Right).
|
Align(lipgloss.Right).
|
||||||
Render(fmt.Sprintf("%d chars", state.textarea.Length()))
|
Render(fmt.Sprintf("%d chars", state.textarea.Length()))
|
||||||
|
|
||||||
box := lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.NormalBorder()).
|
|
||||||
BorderForeground(border).
|
|
||||||
Padding(0, 1).
|
|
||||||
Width(m.refundContentWidth()).
|
|
||||||
Render(state.textarea.View() + "\n" + counter)
|
|
||||||
|
|
||||||
out := lipgloss.JoinVertical(
|
out := lipgloss.JoinVertical(
|
||||||
lipgloss.Left,
|
lipgloss.Left,
|
||||||
m.theme.TextLabel().Render(label),
|
m.theme.TextLabel().Render(label),
|
||||||
box,
|
textareaBox,
|
||||||
|
counter,
|
||||||
)
|
)
|
||||||
if state.messageErr != "" {
|
if state.messageErr != "" {
|
||||||
out += "\n" + m.theme.TextError().Render(state.messageErr)
|
out += "\n" + m.theme.TextError().Render(state.messageErr)
|
||||||
@@ -367,3 +368,7 @@ func (m Model) refundContentWidth() int {
|
|||||||
func (m Model) refundTextareaWidth() int {
|
func (m Model) refundTextareaWidth() int {
|
||||||
return max(1, m.refundContentWidth()-refundTextareaHPad)
|
return max(1, m.refundContentWidth()-refundTextareaHPad)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) refundTextareaHeight() int {
|
||||||
|
return max(refundMinTextareaRows, min(refundMaxTextareaRows, m.heightContainer-refundModalChrome))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user