-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: resend all button for missed posts/emails #2246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
c6e1694
c5d25a6
84b00eb
6d7d549
b610cd1
daa443e
f112065
2794e93
4693bbf
ce377ba
1ddb52c
b949696
85ddd36
c2ba1ab
dd09124
e180b1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,19 +11,27 @@ class CustomersController < Sellers::BaseController | |
| layout "inertia", only: [:index] | ||
|
|
||
| def index | ||
| product = Link.fetch(params[:link_id]) if params[:link_id].present? | ||
| sales = fetch_sales(products: [product].compact) | ||
| customers_presenter = CustomersPresenter.new( | ||
| pundit_user:, | ||
| product:, | ||
| customers: load_sales(sales), | ||
| pagination: { page: 1, pages: (sales.results.total / CUSTOMERS_PER_PAGE.to_f).ceil, next: nil }, | ||
| count: sales.results.total | ||
| ) | ||
| create_user_event("customers_view") | ||
| if !request.inertia_partial? | ||
| product = Link.fetch(params[:link_id]) if params[:link_id].present? | ||
| sales = fetch_sales(products: [product].compact) | ||
| customers_presenter = CustomersPresenter.new( | ||
| pundit_user:, | ||
| product:, | ||
| customers: load_sales(sales), | ||
| pagination: { page: 1, pages: (sales.results.total / CUSTOMERS_PER_PAGE.to_f).ceil, next: nil }, | ||
| count: sales.results.total | ||
| ) | ||
| create_user_event("customers_view") | ||
| end | ||
|
|
||
| purchase = current_seller.sales.find_by_external_id!(params[:purchase_id]) if params[:purchase_id].present? | ||
|
|
||
| render inertia: "Customers/Index", | ||
| props: { customers_presenter: customers_presenter.customers_props } | ||
| render inertia: "Customers/Index", props: { | ||
| customers_presenter: (-> { customers_presenter.customers_props } if !request.inertia_partial?), | ||
| customer_emails: InertiaRails.optional { fetch_customer_emails(purchase) }, | ||
| missed_posts: InertiaRails.optional { CustomerPresenter.new(purchase:).missed_posts(workflow_id: params[:workflow_id]) }, | ||
| workflows: InertiaRails.optional { WorkflowsPresenter.new(seller: current_seller).workflow_options_by_purchase_props(purchase:) }, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved over here as per #2149 (comment) |
||
| }.compact | ||
|
Comment on lines
+14
to
+34
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fetches drawer data on partial visits |
||
| end | ||
|
|
||
| def paged | ||
|
|
@@ -64,55 +72,6 @@ def customer_charges | |
| render json: [] | ||
| end | ||
|
|
||
| def customer_emails | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to |
||
| original_purchase = current_seller.sales.find_by_external_id!(params[:purchase_id]) if params[:purchase_id].present? | ||
|
|
||
| all_purchases = if original_purchase.subscription.present? | ||
| original_purchase.subscription.purchases.all_success_states_except_preorder_auth_and_gift.preload(:receipt_email_info_from_purchase) | ||
| else | ||
| [original_purchase] | ||
| end | ||
|
|
||
| receipts = all_purchases.map do |purchase| | ||
| receipt_email_info = purchase.receipt_email_info | ||
| { | ||
| type: "receipt", | ||
| name: receipt_email_info&.email_name&.humanize || "Receipt", | ||
| id: purchase.external_id, | ||
| state: receipt_email_info&.state&.humanize || "Delivered", | ||
| state_at: receipt_email_info.present? ? receipt_email_info.most_recent_state_at.in_time_zone(current_seller.timezone) : purchase.created_at.in_time_zone(current_seller.timezone), | ||
| url: receipt_purchase_url(purchase.external_id, email: purchase.email), | ||
| date: purchase.created_at | ||
| } | ||
| end | ||
|
|
||
| posts = original_purchase.installments.alive.where(seller_id: original_purchase.seller_id).map do |post| | ||
| email_info = CreatorContactingCustomersEmailInfo.where(purchase: original_purchase, installment: post).last | ||
| { | ||
| type: "post", | ||
| name: post.name, | ||
| id: post.external_id, | ||
| state: email_info.state.humanize, | ||
| state_at: email_info.most_recent_state_at.in_time_zone(current_seller.timezone), | ||
| date: post.published_at | ||
| } | ||
| end | ||
|
|
||
| unpublished_posts = posts.select { |post| post[:date].nil? } | ||
| published_posts = posts - unpublished_posts | ||
| emails = published_posts | ||
| emails = emails.sort_by { |e| -e[:date].to_i } + unpublished_posts | ||
| emails = receipts + emails unless original_purchase.is_bundle_product_purchase? | ||
|
|
||
| render json: emails | ||
| end | ||
|
|
||
| def missed_posts | ||
| purchase = Purchase.where(email: params[:purchase_email].to_s).find_by_external_id!(params[:purchase_id]) | ||
|
|
||
| render json: CustomerPresenter.new(purchase:).missed_posts | ||
| end | ||
|
|
||
| def product_purchases | ||
| purchase = current_seller.sales.find_by_external_id!(params[:purchase_id]) if params[:purchase_id].present? | ||
|
|
||
|
|
@@ -191,4 +150,45 @@ def set_on_page_type | |
| def authorize | ||
| super([:audience, Purchase], :index?) | ||
| end | ||
|
|
||
| def fetch_customer_emails(original_purchase) | ||
| all_purchases = if original_purchase.subscription.present? | ||
| original_purchase.subscription.purchases.all_success_states_except_preorder_auth_and_gift.preload(:receipt_email_info_from_purchase) | ||
| else | ||
| [original_purchase] | ||
| end | ||
|
|
||
| receipts = all_purchases.map do |purchase| | ||
| receipt_email_info = purchase.receipt_email_info | ||
| { | ||
| type: "receipt", | ||
| name: receipt_email_info&.email_name&.humanize || "Receipt", | ||
| id: purchase.external_id, | ||
| state: receipt_email_info&.state&.humanize || "Delivered", | ||
| state_at: receipt_email_info.present? ? receipt_email_info.most_recent_state_at.in_time_zone(current_seller.timezone) : purchase.created_at.in_time_zone(current_seller.timezone), | ||
| url: receipt_purchase_url(purchase.external_id, email: purchase.email), | ||
| date: purchase.created_at | ||
| } | ||
| end | ||
|
|
||
| posts = original_purchase.installments.alive.where(seller_id: original_purchase.seller_id).map do |post| | ||
| email_info = CreatorContactingCustomersEmailInfo.where(purchase: original_purchase, installment: post).last | ||
| { | ||
| type: "post", | ||
| name: post.name, | ||
| id: post.external_id, | ||
| state: email_info.state.humanize, | ||
| state_at: email_info.most_recent_state_at.in_time_zone(current_seller.timezone), | ||
| date: post.published_at | ||
| } | ||
| end | ||
|
|
||
| unpublished_posts = posts.select { |post| post[:date].nil? } | ||
| published_posts = posts - unpublished_posts | ||
| emails = published_posts | ||
| emails = emails.sort_by { |e| -e[:date].to_i } + unpublished_posts | ||
| emails = receipts + emails unless original_purchase.is_bundle_product_purchase? | ||
|
|
||
| emails | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,10 +3,12 @@ | |
| class PostsController < ApplicationController | ||
| include CustomDomainConfig | ||
|
|
||
| before_action :authenticate_user!, only: %i[send_for_purchase] | ||
| after_action :verify_authorized, only: %i[send_for_purchase] | ||
| before_action :authenticate_user!, only: %i[send_for_purchase send_missed_posts] | ||
| after_action :verify_authorized, only: %i[send_for_purchase send_missed_posts] | ||
| before_action :fetch_post, only: %i[send_for_purchase] | ||
| before_action :ensure_seller_is_eligible_to_send_emails, only: %i[send_for_purchase] | ||
| before_action :fetch_purchase, only: %i[send_for_purchase send_missed_posts] | ||
| before_action :ensure_seller_is_eligible_to_send_emails, only: %i[send_for_purchase send_missed_posts] | ||
| before_action :ensure_can_contact_for_purchase, only: %i[send_for_purchase send_missed_posts] | ||
| before_action :set_user_and_custom_domain_config, only: %i[show] | ||
| before_action :check_if_needs_redirect, only: %i[show] | ||
|
|
||
|
|
@@ -54,28 +56,19 @@ def redirect_from_purchase_id | |
| def send_for_purchase | ||
| authorize @post | ||
|
|
||
| purchase = current_seller.sales.find_by_external_id!(params[:purchase_id]) | ||
|
|
||
| # Limit the number of emails sent per post to avoid abuse. | ||
| Rails.cache.fetch("post_email:#{@post.id}:#{purchase.id}", expires_in: 8.hours) do | ||
| CreatorContactingCustomersEmailInfo.where(purchase:, installment: @post).destroy_all | ||
|
|
||
| PostEmailApi.process( | ||
| post: @post, | ||
| recipients: [ | ||
| { | ||
| email: purchase.email, | ||
| purchase:, | ||
| url_redirect: purchase.url_redirect, | ||
| subscription: purchase.subscription, | ||
| }.compact_blank | ||
| ]) | ||
| true | ||
| end | ||
| SendPostsForPurchaseService.send_post(post: @post, purchase: @purchase) | ||
|
|
||
| head :no_content | ||
| end | ||
|
|
||
| def send_missed_posts | ||
| authorize [:audience, @purchase], :send_missed_posts? | ||
|
|
||
| SendPostsForPurchaseService.send_missed_posts_for(purchase: @purchase, workflow_id: params[:workflow_id]) | ||
|
|
||
| render json: { message: "Missed emails are queued for delivery" }, status: :ok | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the purchase opted out, we could return a 422 status with an error toast straight away instead of enqueue-ing the job. This is more coherent from the user's standpoint. We would still need to check that at the job level (in the event of retries for instance).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should already be handled as there is a I'll handle it differently though using validation at service level and change the error code as well |
||
| end | ||
|
|
||
| def increment_post_views | ||
| fetch_post(false) | ||
|
|
||
|
|
@@ -99,14 +92,14 @@ def fetch_post(viewed_by_seller = true) | |
| end | ||
| e404 if @post.blank? | ||
|
|
||
| if viewed_by_seller | ||
| e404 if @post.seller != current_seller | ||
| if @post.seller_id? | ||
| @seller = @post.seller | ||
| else | ||
| @seller = @post.link.seller | ||
|
Comment on lines
+95
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain why you need to set
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is for reasons highlighted over #2149 (comment) and #2149 (comment) |
||
| end | ||
|
|
||
| if @post.seller_id? | ||
| e404 if @post.seller.suspended? | ||
| elsif @post.link_id? | ||
| e404 if @post.link.seller&.suspended? | ||
| if viewed_by_seller | ||
| e404 if @seller != current_seller | ||
| end | ||
| end | ||
|
|
||
|
|
@@ -120,10 +113,22 @@ def check_if_needs_redirect | |
| end | ||
| end | ||
|
|
||
| def fetch_purchase | ||
| @purchase = current_seller.sales.find_by_external_id(params[:purchase_id]) | ||
| return e404_json if @purchase.blank? | ||
|
|
||
| @seller = @purchase.seller | ||
| end | ||
|
|
||
| def ensure_seller_is_eligible_to_send_emails | ||
| seller = @post.seller || @post.link.seller | ||
| unless seller&.eligible_to_send_emails? | ||
| unless @seller&.eligible_to_send_emails? | ||
| render json: { message: "You are not eligible to resend this email." }, status: :unauthorized | ||
| end | ||
| end | ||
|
|
||
| def ensure_can_contact_for_purchase | ||
| unless @purchase.can_contact? | ||
| render json: { message: "This customer has opted out of receiving emails." }, status: :forbidden | ||
| end | ||
| end | ||
| end | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| import { usePage } from "@inertiajs/react"; | ||
| import React from "react"; | ||
|
|
||
| import EmptyState from "$app/components/Admin/EmptyState"; | ||
| import PaginatedLoader, { type Pagination } from "$app/components/Admin/PaginatedLoader"; | ||
| import UserCard, { type User } from "$app/components/Admin/Users/User"; | ||
| import EmptyState from "$app/components/ui/EmptyState"; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-factored as per #2149 (comment) |
||
|
|
||
| type PageProps = { | ||
| users: User[]; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.