diff --git a/config/locales/en.yml b/config/locales/en.yml
index 043fb06b16..c7946d4f1d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -271,6 +271,7 @@ en:
Only owner or admin users are authorized to perform this action
bill_status_billed: You can't bill an entry that has already been billed
create_billed_entry: You can't create a billed timesheet entry
+ validate_billable_project: You can't create an unbilled entry for non-billable projects
internal_server_error: Something went wrong
user_already_member: Email id is already in use. Please enter another email id
updation_not_allowed: updation is not allowed
@@ -292,3 +293,6 @@ en:
sessions:
failure:
invalid: Invalid email or password
+ expenses:
+ update: "Expense updated successfully"
+ destroy: "Expense deleted successfully"
\ No newline at end of file
diff --git a/config/routes/internal_api.rb b/config/routes/internal_api.rb
index c0027dbe4e..169dfcf185 100644
--- a/config/routes/internal_api.rb
+++ b/config/routes/internal_api.rb
@@ -50,7 +50,11 @@
end
end
resources :outstanding_overdue_invoices, only: [:index]
- resources :accounts_aging, only: [:index]
+ resources :accounts_aging, only: [:index] do
+ collection do
+ get :download
+ end
+ end
end
resources :workspaces, only: [:index, :update]
@@ -141,7 +145,7 @@
resources :vendors, only: [:create]
resources :expense_categories, only: [:create]
- resources :expenses, only: [:create, :index, :show]
+ resources :expenses, only: [:create, :index, :show, :update, :destroy]
resources :bulk_previous_employments, only: [:update]
resources :leaves, as: "leave" do
diff --git a/db/migrate/20240304143641_add_stripe_enabled_to_invoices.rb b/db/migrate/20240304143641_add_stripe_enabled_to_invoices.rb
new file mode 100644
index 0000000000..fae6630dc9
--- /dev/null
+++ b/db/migrate/20240304143641_add_stripe_enabled_to_invoices.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStripeEnabledToInvoices < ActiveRecord::Migration[7.0]
+ def change
+ add_column :invoices, :stripe_enabled, :boolean, default: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a781ea059c..6c4ac1f09a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2024_01_29_091400) do
+ActiveRecord::Schema[7.0].define(version: 2024_03_04_143641) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -299,6 +299,7 @@
t.datetime "sent_at"
t.datetime "payment_sent_at"
t.datetime "client_payment_sent_at"
+ t.boolean "stripe_enabled", default: true
t.index ["client_id"], name: "index_invoices_on_client_id"
t.index ["company_id"], name: "index_invoices_on_company_id"
t.index ["discarded_at"], name: "index_invoices_on_discarded_at"
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 38d587fcd4..fbdec317ec 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -3947,9 +3947,9 @@ flux@^4.0.1:
fbjs "^3.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.7:
- version "1.15.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
- integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
fork-ts-checker-webpack-plugin@^6.5.0:
version "6.5.2"
diff --git a/spec/models/concerns/leave_type_validatable_spec.rb b/spec/models/concerns/leave_type_validatable_spec.rb
new file mode 100644
index 0000000000..450ef4da08
--- /dev/null
+++ b/spec/models/concerns/leave_type_validatable_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe LeaveTypeValidatable, type: :model do
+ let(:leave) { create(:leave) }
+
+ describe "Custom Validations" do
+ context "when validating allocation combinations" do
+ it "is not valid with weekly allocation period and weekly frequency" do
+ leave_type = build(:leave_type, allocation_period: "weeks", allocation_frequency: "per_week", leave:)
+ expect(leave_type.valid?).to be false
+ expect(leave_type.errors[:base]).to include(
+ "Invalid combination: Allocation period in weeks cannot have frequency per week")
+ end
+
+ it "is not valid with monthly allocation period and weekly or monthly frequencies" do
+ ["per_week", "per_month"].each do |freq|
+ leave_type = build(:leave_type, allocation_period: "months", allocation_frequency: freq, leave:)
+ expect(leave_type.valid?).to be false
+ expect(leave_type.errors[:base]).to include(
+ "Invalid combination: Allocation period in months can only have frequency per quarter or per year")
+ end
+ end
+
+ it "is valid with monthly allocation period and quarterly or yearly frequencies" do
+ ["per_quarter", "per_year"].each do |freq|
+ leave_type = build(
+ :leave_type, allocation_period: "months", allocation_frequency: freq, allocation_value: 2,
+ leave:)
+ expect(leave_type.valid?).to be true
+ end
+ end
+ end
+
+ context "when validating allocation values" do
+ it "is not valid with an allocation value exceeding the limit for the period and frequency" do
+ combinations = {
+ ["days", "per_week"] => 8,
+ ["weeks", "per_month"] => 6
+ }
+ combinations.each do |(period, freq), value|
+ leave_type = build(
+ :leave_type, allocation_period: period, allocation_frequency: freq,
+ allocation_value: value, leave:)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:allocation_value]).to include(
+ "cannot exceed #{value - 1} #{period} for #{freq} frequency")
+ end
+ end
+
+ it "is valid with an allocation value within the limit for the period and frequency" do
+ combinations = {
+ ["days", "per_week"] => 7,
+ ["weeks", "per_month"] => 5
+ }
+ combinations.each do |(period, freq), value|
+ leave_type = build(
+ :leave_type, allocation_period: period, allocation_frequency: freq,
+ allocation_value: value, leave:)
+ expect(leave_type).to be_valid
+ end
+ end
+ end
+
+ context "when validating carry forward limits with frequency considerations" do
+ it "is valid when carry_forward is less than total days in a year for days per week" do
+ leave_type = build(
+ :leave_type, allocation_period: "days", allocation_frequency: "per_week",
+ allocation_value: 4, carry_forward_days: 5, leave:)
+ expect(leave_type).to be_valid
+ end
+
+ it "is not valid when carry_forward exceeds total days for weeks per year" do
+ leave_type = build(
+ :leave_type, allocation_period: "weeks", allocation_frequency: "per_year",
+ allocation_value: 2, carry_forward_days: 15, leave:)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:carry_forward_days]).to include("cannot exceed the total allocated days")
+ end
+
+ it "is valid when carry_forward does not exceed total days for months per quarter" do
+ leave_type = build(
+ :leave_type, allocation_period: "months", allocation_frequency: "per_quarter",
+ allocation_value: 1, carry_forward_days: 30, leave:)
+ expect(leave_type).to be_valid
+ end
+
+ it "is not valid when carry_forward exceeds total days for months per year" do
+ leave_type = build(
+ :leave_type, allocation_period: "months", allocation_frequency: "per_year",
+ allocation_value: 2, carry_forward_days: 63, leave:)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:carry_forward_days]).to include("cannot exceed the total allocated days")
+ end
+ end
+ end
+end
diff --git a/spec/models/holiday_info_spec.rb b/spec/models/holiday_info_spec.rb
index 967cdf40f1..3e18dd720b 100644
--- a/spec/models/holiday_info_spec.rb
+++ b/spec/models/holiday_info_spec.rb
@@ -23,12 +23,6 @@
it { is_expected.to validate_presence_of(:date) }
it { is_expected.to validate_presence_of(:category) }
- it 'validates optional holidays when the category is "optional"' do
- optional_holiday_info.holiday.enable_optional_holidays = false
- optional_holiday_info.valid?
- expect(optional_holiday_info.errors[:base]).to include("optional holidays are disabled")
- end
-
it "validates the year of the date" do
invalid_holiday_info = build(:holiday_info, holiday:, date: "2022-01-01")
invalid_holiday_info.valid?
diff --git a/spec/models/leave_type_spec.rb b/spec/models/leave_type_spec.rb
index bd22aad41a..dab14a51c9 100644
--- a/spec/models/leave_type_spec.rb
+++ b/spec/models/leave_type_spec.rb
@@ -6,53 +6,73 @@
let(:leave) { create(:leave) }
describe "validations" do
- it "is valid with valid attributes" do
- leave_type = build(:leave_type, leave:)
- expect(leave_type).to be_valid
- end
-
- it "is not valid without a name" do
- leave_type = build(:leave_type, name: nil, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:name]).to include("can't be blank")
- end
-
- it "is not valid with a duplicate color within the same leave" do
- existing_leave_type = create(:leave_type, leave:)
- leave_type = build(:leave_type, color: existing_leave_type.color, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:color]).to include("has already been taken for this leave")
- end
-
- it "is not valid with a duplicate icon within the same leave" do
- existing_leave_type = create(:leave_type, leave:)
- leave_type = build(:leave_type, icon: existing_leave_type.icon, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:icon]).to include("has already been taken for this leave")
- end
-
- it "is not valid without a allocation value" do
- leave_type = build(:leave_type, allocation_value: nil, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:allocation_value]).to include("can't be blank")
- end
-
- it "is not valid if allocation value is less than 1" do
- leave_type = build(:leave_type, allocation_value: 0, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:allocation_value]).to include("must be greater than or equal to 1")
- end
-
- it "is not valid without a allocation frequency" do
- leave_type = build(:leave_type, allocation_frequency: nil, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:allocation_frequency]).to include("can't be blank")
- end
-
- it "is not valid without a carry forward days" do
- leave_type = build(:leave_type, carry_forward_days: nil, leave:)
- expect(leave_type).not_to be_valid
- expect(leave_type.errors[:carry_forward_days]).to include("can't be blank")
- end
+ it "is valid with valid attributes" do
+ leave_type = build(
+ :leave_type, leave:, allocation_period: "weeks", allocation_frequency: "per_year",
+ allocation_value: 2)
+ expect(leave_type).to be_valid
end
+
+ it "is not valid without a name" do
+ leave_type = build(
+ :leave_type, name: nil, leave:, allocation_period: "days", allocation_frequency: "per_week",
+ allocation_value: 5)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:name]).to include("can't be blank")
+ end
+
+ it "is not valid with a duplicate color within the same leave" do
+ existing_leave_type = create(
+ :leave_type, leave:, allocation_period: "months",
+ allocation_frequency: "per_quarter", allocation_value: 1)
+ leave_type = build(
+ :leave_type, color: existing_leave_type.color, leave:, allocation_period: "months",
+ allocation_frequency: "per_quarter", allocation_value: 1)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:color]).to include("has already been taken for this leave")
+ end
+
+ it "is not valid with a duplicate icon within the same leave" do
+ existing_leave_type = create(
+ :leave_type, leave:, allocation_period: "weeks", allocation_frequency: "per_month",
+ allocation_value: 2)
+ leave_type = build(
+ :leave_type, icon: existing_leave_type.icon, leave:, allocation_period: "weeks",
+ allocation_frequency: "per_month", allocation_value: 2)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:icon]).to include("has already been taken for this leave")
+ end
+
+ it "is not valid without an allocation value" do
+ leave_type = build(
+ :leave_type, allocation_value: nil, leave:, allocation_period: "days",
+ allocation_frequency: "per_week")
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:allocation_value]).to include("can't be blank")
+ end
+
+ it "is not valid if allocation value is less than 1" do
+ leave_type = build(
+ :leave_type, allocation_value: 0, leave:, allocation_period: "days",
+ allocation_frequency: "per_month")
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:allocation_value]).to include("must be greater than or equal to 1")
+ end
+
+ it "is not valid without an allocation frequency" do
+ leave_type = build(
+ :leave_type, allocation_frequency: nil, leave:, allocation_period: "weeks",
+ allocation_value: 3)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:allocation_frequency]).to include("can't be blank")
+ end
+
+ it "is not valid without carry forward days" do
+ leave_type = build(
+ :leave_type, carry_forward_days: nil, leave:, allocation_period: "months",
+ allocation_frequency: "per_year", allocation_value: 2)
+ expect(leave_type).not_to be_valid
+ expect(leave_type.errors[:carry_forward_days]).to include("can't be blank")
+ end
+end
end
diff --git a/spec/models/timeoff_entry_spec.rb b/spec/models/timeoff_entry_spec.rb
index e05fa7f335..8d326256cc 100644
--- a/spec/models/timeoff_entry_spec.rb
+++ b/spec/models/timeoff_entry_spec.rb
@@ -3,7 +3,42 @@
require "rails_helper"
RSpec.describe TimeoffEntry, type: :model do
+ let(:company) { create(:company) }
+ let(:user) { create(:user, current_workspace_id: company.id) }
+ let(:holiday) { create(:holiday, year: Date.current.year, company:) }
+ let(:national_holiday) { create(:holiday_info, date: Date.current, category: "national", holiday:) }
+ let(:optional_holiday) { create(:holiday_info, date: Date.current, category: "optional", holiday:) }
+
describe "validations" do
+ before do
+ @existing_timeoff_entry = create(
+ :timeoff_entry,
+ user_id: user.id,
+ leave_type_id: nil,
+ holiday_info_id: optional_holiday.id,
+ duration: 400,
+ leave_date: Date.current
+ )
+
+ @new_time_off_entry = build(
+ :timeoff_entry,
+ user_id: user.id,
+ leave_type_id: nil,
+ holiday_info_id: optional_holiday.id,
+ duration: 400,
+ leave_date: Date.current,
+ )
+
+ @new_optional_time_off_entry = build(
+ :timeoff_entry,
+ user_id: user.id,
+ leave_type_id: nil,
+ holiday_info_id: optional_holiday.id,
+ duration: 400,
+ leave_date: Date.current + 1,
+ )
+ end
+
it { is_expected.to validate_presence_of(:duration) }
it do
@@ -13,6 +48,18 @@
end
it { is_expected.to validate_presence_of(:leave_date) }
+
+ it "is not valid if adding two holidays on the same day" do
+ expect(@new_time_off_entry).not_to be_valid
+ expect(@new_time_off_entry.errors[:base]).to include(
+ "You are adding two holidays on the same day, please recheck")
+ end
+
+ it "is not valid if it exceeds the maximum number of permitted optional holidays" do
+ expect(@new_optional_time_off_entry).not_to be_valid
+ expect(@new_optional_time_off_entry.errors[:base]).to include(
+ "You have exceeded the maximum number of permitted optional holidays")
+ end
end
describe "associations" do
diff --git a/spec/models/timesheet_entry_spec.rb b/spec/models/timesheet_entry_spec.rb
index bef2f0983f..de5ac559a1 100644
--- a/spec/models/timesheet_entry_spec.rb
+++ b/spec/models/timesheet_entry_spec.rb
@@ -9,6 +9,7 @@
let(:client2) { create(:client, company_id: company2.id) }
let(:project) { create(:project, client_id: client.id) }
let(:project2) { create(:project, client_id: client2.id) }
+ let(:billable_project) { create(:project, billable: true, client_id: client.id) }
let(:timesheet_entry) { create(:timesheet_entry, project:) }
describe "Associations" do
@@ -97,6 +98,26 @@
end
end
+ describe "#validate_billable_project" do
+ let(:error_message) { "You can't create an unbilled entry for non-billable projects" }
+
+ context "when the project is non-billable" do
+ it "returns an error if project is non billable and user try to create unbilled entry" do
+ timesheet_entry = build(:timesheet_entry, bill_status: "unbilled")
+ expect(timesheet_entry.valid?).to be_falsey
+ expect(timesheet_entry.errors.messages[:base]).to include(error_message)
+ end
+ end
+
+ context "when the project is billable" do
+ it "does not return an error if the user tries to create an unbilled entry" do
+ timesheet_entry = build(:timesheet_entry, bill_status: "unbilled", project: billable_project)
+ expect(timesheet_entry.valid?).to be_truthy
+ expect(timesheet_entry.errors.messages[:base]).not_to include(error_message)
+ end
+ end
+ end
+
describe "#ensure_billed_status_should_not_be_changed" do
context "when admin or owner is updating the time entry" do
before do
@@ -106,7 +127,7 @@
end
let(:admin) { create(:user) }
- let(:timesheet_entry) { create(:timesheet_entry, project:) }
+ let(:timesheet_entry) { create(:timesheet_entry, project: billable_project) }
context "when time entry is billed" do
it "allows owners and admins to edit the billed time entry" do
@@ -160,7 +181,7 @@
end
let(:user) { create(:user) }
- let(:timesheet_entry) { create(:timesheet_entry, project:) }
+ let(:timesheet_entry) { create(:timesheet_entry, project: billable_project) }
let(:error_message) { "You can't bill an entry that has already been billed" }
context "when time entry is billed" do
diff --git a/spec/policies/invoice_policy_spec.rb b/spec/policies/invoice_policy_spec.rb
index 168e0b0125..bf3175de26 100644
--- a/spec/policies/invoice_policy_spec.rb
+++ b/spec/policies/invoice_policy_spec.rb
@@ -108,7 +108,7 @@
let(:attributes) do
%i[
issue_date due_date status invoice_number reference amount outstanding_amount
- tax amount_paid amount_due discount client_id external_view_key
+ tax amount_paid amount_due discount client_id external_view_key stripe_enabled
].push(invoice_line_items_attributes:)
end
diff --git a/spec/requests/internal_api/v1/expenses/index_spec.rb b/spec/requests/internal_api/v1/expenses/index_spec.rb
index 44067617b2..60b390d2a5 100644
--- a/spec/requests/internal_api/v1/expenses/index_spec.rb
+++ b/spec/requests/internal_api/v1/expenses/index_spec.rb
@@ -53,7 +53,8 @@
"date" => CompanyDateFormattingService.new(expense.date, company:).process,
"expenseType" => expense.expense_type,
"categoryName" => expense.expense_category.name,
- "vendorName" => expense.vendor&.name
+ "vendorName" => expense.vendor&.name,
+ "description" => expense.description
}
end
expect(json_response["expenses"]).to eq(expected_data)
diff --git a/spec/requests/internal_api/v1/generate_invoice/index_spec.rb b/spec/requests/internal_api/v1/generate_invoice/index_spec.rb
index 9802e53127..9315fad64e 100644
--- a/spec/requests/internal_api/v1/generate_invoice/index_spec.rb
+++ b/spec/requests/internal_api/v1/generate_invoice/index_spec.rb
@@ -6,7 +6,7 @@
let(:company) { create(:company) }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:client) { create(:client, company:) }
- let!(:project) { create(:project, client:) }
+ let!(:project) { create(:project, billable: true, client:) }
let!(:project_member) { create(:project_member, user:, project:) }
let!(:timesheet_entry) { create(:timesheet_entry, user:, project:, bill_status: "unbilled") }
diff --git a/spec/requests/internal_api/v1/invoices/create_spec.rb b/spec/requests/internal_api/v1/invoices/create_spec.rb
index 342bff8c18..4a83738bc1 100644
--- a/spec/requests/internal_api/v1/invoices/create_spec.rb
+++ b/spec/requests/internal_api/v1/invoices/create_spec.rb
@@ -37,10 +37,10 @@
it "creates invoice successfully & reindex it" do
send_request :post, internal_api_v1_invoices_path(invoice:), headers: auth_headers(user)
expect(response).to have_http_status(:ok)
- expected_attrs = ["amount", "amountDue", "amountPaid",
- "client", "discount", "dueDate", "id",
- "invoiceLineItems", "invoiceNumber", "issueDate",
- "outstandingAmount", "reference", "status", "tax"]
+ expected_attrs =
+ ["amount", "amountDue", "amountPaid", "client", "discount", "dueDate",
+ "id", "invoiceLineItems", "invoiceNumber", "issueDate",
+ "outstandingAmount", "reference", "status", "stripeEnabled", "tax"]
expect(json_response.keys.sort).to match(expected_attrs)
Invoice.reindex
assert_equal ["SAI-C1-03"], Invoice.search("SAI-C1-03").map(&:invoice_number)
diff --git a/spec/requests/internal_api/v1/reports/time_entry/index_spec.rb b/spec/requests/internal_api/v1/reports/time_entry/index_spec.rb
index 150bb000d2..2c4290708d 100644
--- a/spec/requests/internal_api/v1/reports/time_entry/index_spec.rb
+++ b/spec/requests/internal_api/v1/reports/time_entry/index_spec.rb
@@ -6,9 +6,9 @@
let(:company) { create(:company, name: "company_one") }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:client) { create(:client, :with_logo, company:, name: "American_Client") }
- let(:project) { create(:project, client:, name: "A class project") }
+ let(:project) { create(:project, billable: true, client:, name: "A class project") }
let(:client2) { create(:client, company_id: company.id, name: "European_Client") }
- let(:project2) { create(:project, client_id: client2.id, name: "B class project") }
+ let(:project2) { create(:project, billable: true, client_id: client2.id, name: "B class project") }
let(:client3) { create(:client, company_id: company.id, name: "Indian_Client") }
let(:project3) { create(:project, client_id: client3.id, name: "C class project") }
let(:last_month_start_date) { 1.month.ago.beginning_of_month + 1.days }
diff --git a/spec/requests/internal_api/v1/time_tracking/index_spec.rb b/spec/requests/internal_api/v1/time_tracking/index_spec.rb
index 2827ed0c30..628555627a 100644
--- a/spec/requests/internal_api/v1/time_tracking/index_spec.rb
+++ b/spec/requests/internal_api/v1/time_tracking/index_spec.rb
@@ -13,6 +13,9 @@
let!(:project2) { create(:project, client: client1) }
let!(:project3) { create(:project, client: client2) }
let!(:project4) { create(:project, client: client3) }
+ let!(:holiday) { create(:holiday, year: Date.current.year, company: company1) }
+ let(:national_holiday) { create(:holiday_info, date: Date.current, category: "national", holiday:) }
+ let(:optional_holiday) { create(:holiday_info, date: Date.current + 2.days, category: "optional", holiday:) }
before do
create(:project_member, user:, project: project1)
diff --git a/spec/requests/internal_api/v1/timeoff_entries/create_spec.rb b/spec/requests/internal_api/v1/timeoff_entries/create_spec.rb
index 136b87bc0b..f04e8a0d17 100644
--- a/spec/requests/internal_api/v1/timeoff_entries/create_spec.rb
+++ b/spec/requests/internal_api/v1/timeoff_entries/create_spec.rb
@@ -6,7 +6,10 @@
let(:company) { create(:company) }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:leave) { create(:leave, company:) }
- let!(:leave_type) { create(:leave_type, name: "Annual", leave:) }
+ let!(:leave_type) { create(
+ :leave_type, name: "Annual", leave:, allocation_period: "months", allocation_frequency: "per_quarter",
+ allocation_value: 1, carry_forward_days: 30,)
+}
context "when user is an admin" do
before do
diff --git a/spec/requests/internal_api/v1/timeoff_entries/destroy_spec.rb b/spec/requests/internal_api/v1/timeoff_entries/destroy_spec.rb
index d040ce8800..683395da98 100644
--- a/spec/requests/internal_api/v1/timeoff_entries/destroy_spec.rb
+++ b/spec/requests/internal_api/v1/timeoff_entries/destroy_spec.rb
@@ -6,7 +6,10 @@
let(:company) { create(:company) }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:leave) { create(:leave, company:) }
- let!(:leave_type) { create(:leave_type, leave:) }
+ let!(:leave_type) { create(
+ :leave_type, leave:, allocation_period: "months", allocation_frequency: "per_quarter",
+ allocation_value: 1, carry_forward_days: 30,)
+}
let!(:timeoff_entry) { create(:timeoff_entry, user:, leave_type:) }
context "when user is an admin" do
diff --git a/spec/requests/internal_api/v1/timeoff_entries/update_spec.rb b/spec/requests/internal_api/v1/timeoff_entries/update_spec.rb
index d9727f3c84..f72a072f19 100644
--- a/spec/requests/internal_api/v1/timeoff_entries/update_spec.rb
+++ b/spec/requests/internal_api/v1/timeoff_entries/update_spec.rb
@@ -6,7 +6,10 @@
let(:company) { create(:company) }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:leave) { create(:leave, company:) }
- let!(:leave_type) { create(:leave_type, name: "Annaul", leave:) }
+ let!(:leave_type) { create(
+ :leave_type, name: "Annaul", leave:, allocation_period: "months", allocation_frequency: "per_quarter",
+ allocation_value: 1, carry_forward_days: 30,)
+}
let!(:timeoff_entry) { create(:timeoff_entry, user:, leave_type:) }
context "when user is an admin" do
diff --git a/spec/requests/internal_api/v1/timesheet_entries/create_spec.rb b/spec/requests/internal_api/v1/timesheet_entries/create_spec.rb
index 4098ecb7c2..30177a30cf 100644
--- a/spec/requests/internal_api/v1/timesheet_entries/create_spec.rb
+++ b/spec/requests/internal_api/v1/timesheet_entries/create_spec.rb
@@ -6,7 +6,7 @@
let(:company) { create(:company) }
let(:user) { create(:user, current_workspace_id: company.id) }
let(:client) { create(:client, company:) }
- let(:project) { create(:project, client:) }
+ let(:project) { create(:project, billable: true, client:) }
context "when user is an admin" do
before do
diff --git a/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb b/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb
index 9f935c1b44..d4d581e8b6 100644
--- a/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb
+++ b/spec/requests/internal_api/v1/timesheet_entries/update_spec.rb
@@ -7,7 +7,7 @@
let(:user) { create(:user, current_workspace_id: company.id) }
let(:user2) { create(:user, current_workspace_id: company.id) }
let(:client) { create(:client, company:) }
- let(:project) { create(:project, client:) }
+ let(:project) { create(:project, billable: true, client:) }
let!(:timesheet_entry) {
create(
:timesheet_entry,
diff --git a/spec/services/reports/accounts_aging/download_service_spec.rb b/spec/services/reports/accounts_aging/download_service_spec.rb
new file mode 100644
index 0000000000..09878446c3
--- /dev/null
+++ b/spec/services/reports/accounts_aging/download_service_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Reports::AccountsAging::DownloadService do
+ let(:current_company) { create(:company) }
+
+ describe "#process" do
+ let(:params) { { some_param: "value" } }
+ let(:reports_data) { { clients: [], total_amount_overdue_by_date_range: {} } }
+
+ subject { described_class.new(params, current_company) }
+
+ before do
+ allow(Reports::AccountsAging::FetchOverdueAmount).to receive(:new).and_return(
+ double(
+ "FetchOverdueAmount",
+ process: reports_data))
+ allow(Reports::GeneratePdf).to receive(:new).and_return(double("Reports::GeneratePdf", process: nil))
+ allow(Reports::GenerateCsv).to receive(:new).and_return(double("Reports::GenerateCsv", process: nil))
+ end
+
+ it "fetches complete report, generates PDF and CSV" do
+ allow(subject).to receive(:fetch_complete_report)
+ allow(subject).to receive(:generate_pdf)
+ allow(subject).to receive(:generate_csv)
+
+ subject.process
+ end
+
+ it "fetches complete report and generates CSV" do
+ allow(subject).to receive(:fetch_complete_report)
+ allow(subject).to receive(:generate_csv)
+
+ subject.process
+ end
+ end
+end
diff --git a/spec/services/reports/generate_csv_spec.rb b/spec/services/reports/generate_csv_spec.rb
new file mode 100644
index 0000000000..7feb8181e6
--- /dev/null
+++ b/spec/services/reports/generate_csv_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Reports::GenerateCsv do
+ describe "#process" do
+ let(:headers) { ["Name", "Age", "Email"] }
+ let(:data) do
+ [
+ ["John Doe", "30", "john@example.com"],
+ ["Jane Smith", "25", "jane@example.com"]
+ ]
+ end
+
+ subject { described_class.new(data, headers) }
+
+ it "generates CSV data with headers and data" do
+ csv_data = subject.process
+ parsed_csv = CSV.parse(csv_data)
+
+ expect(parsed_csv.first).to eq(headers)
+
+ expect(parsed_csv[1]).to eq(data.first)
+ expect(parsed_csv[2]).to eq(data.second)
+ end
+ end
+end
diff --git a/spec/services/reports/generate_pdf_spec.rb b/spec/services/reports/generate_pdf_spec.rb
new file mode 100644
index 0000000000..be927b3f81
--- /dev/null
+++ b/spec/services/reports/generate_pdf_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Reports::GeneratePdf do
+ let(:report_data) { double("report_data") }
+ let(:current_company) { double("current_company") }
+
+ describe "#process" do
+ context "when report type is time_entries" do
+ subject { described_class.new(:time_entries, report_data, current_company) }
+
+ it "generates PDF for time entries" do
+ allow(Pdf::HtmlGenerator).to receive(:new).with(
+ :time_entries,
+ locals: { report_data:, current_company: }).and_return(double("Pdf::HtmlGenerator", make: nil))
+ subject.process
+ end
+ end
+
+ context "when report type is accounts_aging" do
+ subject { described_class.new(:accounts_aging, report_data, current_company) }
+
+ it "generates PDF for accounts aging" do
+ allow(Pdf::HtmlGenerator).to receive(:new).with(
+ :accounts_aging,
+ locals: { report_data:, current_company: }).and_return(double("Pdf::HtmlGenerator", make: nil))
+ subject.process
+ end
+ end
+
+ context "when report type is unsupported" do
+ it "raises ArgumentError" do
+ expect {
+ described_class.new(:unsupported_report_type, report_data, current_company).process
+ }.to raise_error(ArgumentError, "Unsupported report type: unsupported_report_type")
+ end
+ end
+ end
+end
diff --git a/spec/services/reports/time_entries/download_service_spec.rb b/spec/services/reports/time_entries/download_service_spec.rb
index ed1e9a16f7..6a2f79a47d 100644
--- a/spec/services/reports/time_entries/download_service_spec.rb
+++ b/spec/services/reports/time_entries/download_service_spec.rb
@@ -6,6 +6,10 @@
let(:company) { create(:company) }
let(:client) { create(:client, :with_logo, company:) }
let(:project) { create(:project, client:) }
+ let(:csv_headers) do
+ "Project,Client,Note,Team Member,Date,Hours Logged"
+ end
+ let(:report_entries) { [double("TimeEntry")] }
before do
create_list(:user, 12)
@@ -31,5 +35,20 @@
all_users_with_name = User.all.order(:first_name).map { |u| u.full_name }
expect(data.pluck(:label)).to eq(all_users_with_name)
end
+
+ it "generates CSV report" do
+ data = subject.process
+ expect(data).to include(csv_headers)
+ end
+
+ it "generates a PDF report using Pdf::HtmlGenerator" do
+ subject { described_class.new(report_entries, current_company) }
+
+ html_generator = instance_double("Pdf::HtmlGenerator")
+ allow(Pdf::HtmlGenerator).to receive(:new).and_return(html_generator)
+
+ allow(html_generator).to receive(:make)
+ subject.process
+ end
end
end
diff --git a/spec/services/reports/time_entries/generate_csv_spec.rb b/spec/services/reports/time_entries/generate_csv_spec.rb
deleted file mode 100644
index abcaf43bf2..0000000000
--- a/spec/services/reports/time_entries/generate_csv_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require "rails_helper"
-
-RSpec.describe Reports::TimeEntries::GenerateCsv do
- let(:company) { create(:company) }
- let!(:entry) { create(:timesheet_entry) }
-
- describe "#process" do
- before do
- TimesheetEntry.reindex
- end
-
- subject { described_class.new(TimesheetEntry.search(load: false), company).process }
-
- let(:csv_headers) do
- "Project,Client,Note,Team Member,Date,Hours Logged"
- end
- let(:csv_data) do
- "#{entry.project_name}," \
- "#{entry.client_name}," \
- "#{entry.note}," \
- "#{entry.user_full_name}," \
- "#{entry.formatted_work_date}," \
- "#{entry.formatted_duration}"
- end
-
- it "returns CSV string" do
- expect(subject).to include(csv_headers)
- expect(subject).to include(csv_data)
- end
- end
-end
diff --git a/spec/services/reports/time_entries/generate_pdf_spec.rb b/spec/services/reports/time_entries/generate_pdf_spec.rb
deleted file mode 100644
index 09e1486b89..0000000000
--- a/spec/services/reports/time_entries/generate_pdf_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require "rails_helper"
-
-RSpec.describe Reports::TimeEntries::GeneratePdf do
- let(:report_entries) { [double("TimeEntry")] }
- let(:current_company) { double("Company") }
-
- subject { described_class.new(report_entries, current_company) }
-
- describe "#process" do
- it "generates a PDF report using Pdf::HtmlGenerator" do
- html_generator = instance_double("Pdf::HtmlGenerator")
- allow(Pdf::HtmlGenerator).to receive(:new).and_return(html_generator)
-
- allow(html_generator).to receive(:make)
- subject.process
- end
- end
-end
diff --git a/spec/services/timeoff_entries/index_service_spec.rb b/spec/services/timeoff_entries/index_service_spec.rb
new file mode 100644
index 0000000000..579b8738ba
--- /dev/null
+++ b/spec/services/timeoff_entries/index_service_spec.rb
@@ -0,0 +1,304 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe TimeoffEntries::IndexService do # rubocop:disable RSpec/FilePath
+ let(:company) { create(:company) }
+ let(:user) { create(:user, current_workspace_id: company.id) }
+ let!(:leave) { create(:leave, company:, year: Date.today.year) }
+ let!(:leave_type) {
+ create(
+ :leave_type,
+ name: "Annual",
+ allocation_value: 2,
+ icon: LeaveType.icons[:calendar],
+ color: LeaveType.colors[:chart_blue],
+ allocation_period: :days,
+ allocation_frequency: :per_month,
+ carry_forward_days: 5,
+ leave:
+ )
+ }
+ let!(:leave_type_days_per_week) {
+ create(
+ :leave_type,
+ name: "Annual",
+ icon: LeaveType.icons[:cake],
+ color: LeaveType.colors[:chart_pink],
+ allocation_value: 2,
+ allocation_period: :days,
+ allocation_frequency: :per_week,
+ carry_forward_days: 2,
+ leave:
+ )
+ }
+
+ let!(:leave_type_days_per_quarter) {
+ create(
+ :leave_type,
+ name: "Annual",
+ icon: LeaveType.icons[:car],
+ color: LeaveType.colors[:chart_green],
+ allocation_value: 2,
+ allocation_period: :days,
+ allocation_frequency: :per_quarter,
+ carry_forward_days: 2,
+ leave:
+ )
+ }
+
+ let!(:leave_type_days_per_year) {
+ create(
+ :leave_type,
+ name: "Annual",
+ icon: LeaveType.icons[:medicine],
+ color: LeaveType.colors[:chart_orange],
+ allocation_value: 2,
+ allocation_period: :days,
+ allocation_frequency: :per_year,
+ carry_forward_days: 2,
+ leave:
+ )
+ }
+
+ let!(:timeoff_entry) { # rubocop:disable RSpec/LetSetup
+ create(
+ :timeoff_entry,
+ duration: 60,
+ leave_date: Date.today,
+ user:,
+ leave_type:)
+ }
+
+ describe "#initialize" do
+ it "checks preset values in initialize method" do
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+
+ expect(service.current_user.present?).to be true
+ expect(service.current_company.present?).to be true
+ expect(service.user_id.present?).to be true
+ expect(service.year.present?).to be true
+ end
+ end
+
+ describe "#process" do
+ before do
+ create(:employment, company:, user:)
+ user.add_role :admin, company
+
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+ @data = service.process
+ end
+
+ it "checks for all timeoff entries of current user" do
+ timeoff_entries ||= company.timeoff_entries.includes([:leave_type])
+ .where(user_id: user.id)
+ .order(leave_date: :desc)
+
+ expect(@data[:timeoff_entries]).to eq(timeoff_entries)
+ end
+
+ it "returns all the employees if the current user is admin" do
+ expect(@data[:employees]).to eq(company.users)
+ end
+
+ it "returns total timeoff entries duration" do
+ timeoff_entries_duration ||= company.timeoff_entries.includes([:leave_type])
+ .where(user_id: user.id)
+ .order(leave_date: :desc)
+ .sum(:duration)
+
+ expect(@data[:total_timeoff_entries_duration]).to eq(timeoff_entries_duration)
+ end
+
+ it "returns leave balance for days per month" do
+ summary_object = {
+ id: leave_type.id,
+ name: leave_type.name,
+ icon: leave_type.icon,
+ color: leave_type.color,
+ total_leave_type_days: 6,
+ timeoff_entries_duration: 60,
+ net_duration: 2820,
+ net_days: 5
+ }
+
+ expect(@data[:leave_balance][0]).to eq(summary_object)
+ end
+
+ it "returns leave balance for days per week" do
+ summary_object = {
+ id: leave_type_days_per_week.id,
+ name: leave_type_days_per_week.name,
+ icon: leave_type_days_per_week.icon,
+ color: leave_type_days_per_week.color,
+ total_leave_type_days: 24,
+ timeoff_entries_duration: 0,
+ net_duration: 11520,
+ net_days: 24
+ }
+
+ expect(@data[:leave_balance][1]).to eq(summary_object)
+ end
+
+ it "returns leave balance for days per quarter" do
+ summary_object = {
+ id: leave_type_days_per_quarter.id,
+ name: leave_type_days_per_quarter.name,
+ icon: leave_type_days_per_quarter.icon,
+ color: leave_type_days_per_quarter.color,
+ total_leave_type_days: 2,
+ timeoff_entries_duration: 0,
+ net_duration: 960,
+ net_days: 2
+ }
+
+ expect(@data[:leave_balance][2]).to eq(summary_object)
+ end
+
+ it "returns leave balance for days per year" do
+ summary_object = {
+ id: leave_type_days_per_year.id,
+ name: leave_type_days_per_year.name,
+ icon: leave_type_days_per_year.icon,
+ color: leave_type_days_per_year.color,
+ total_leave_type_days: 2,
+ timeoff_entries_duration: 0,
+ net_duration: 960,
+ net_days: 2
+ }
+
+ expect(@data[:leave_balance][3]).to eq(summary_object)
+ end
+ end
+
+ describe "#process days per month when joining date is current year" do
+ before do
+ create(:employment, company:, user:, joined_at: Date.new(Time.current.year, 1, 16), resigned_at: nil)
+ user.add_role :admin, company
+
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+ @data = service.process
+ end
+
+ it "returns leave balance for days per month" do
+ summary_object = {
+ id: leave_type.id,
+ name: leave_type.name,
+ icon: leave_type.icon,
+ color: leave_type.color,
+ total_leave_type_days: 5,
+ timeoff_entries_duration: 60,
+ net_duration: 2340,
+ net_days: 4
+ }
+
+ expect(@data[:leave_balance][0]).to eq(summary_object)
+ end
+ end
+
+ describe "#process days per week when joining date is current year" do
+ before do
+ create(:employment, company:, user:, joined_at: Date.new(Time.current.year, 1, 5), resigned_at: nil)
+ user.add_role :admin, company
+
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+ @data = service.process
+ end
+
+ it "returns leave balance for days per week" do
+ summary_object = {
+ id: leave_type_days_per_week.id,
+ name: leave_type_days_per_week.name,
+ icon: leave_type_days_per_week.icon,
+ color: leave_type_days_per_week.color,
+ total_leave_type_days: 23,
+ timeoff_entries_duration: 0,
+ net_duration: 11040,
+ net_days: 23
+ }
+
+ expect(@data[:leave_balance][1]).to eq(summary_object)
+ end
+ end
+
+ describe "#process days per quarter when joining date is current year" do
+ before do
+ create(:employment, company:, user:, joined_at: Date.new(Time.current.year, 2, 17), resigned_at: nil)
+ user.add_role :admin, company
+
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+ @data = service.process
+ end
+
+ it "returns leave balance for days per quarter" do
+ summary_object = {
+ id: leave_type_days_per_quarter.id,
+ name: leave_type_days_per_quarter.name,
+ icon: leave_type_days_per_quarter.icon,
+ color: leave_type_days_per_quarter.color,
+ total_leave_type_days: 1,
+ timeoff_entries_duration: 0,
+ net_duration: 480,
+ net_days: 1
+ }
+
+ expect(@data[:leave_balance][2]).to eq(summary_object)
+ end
+ end
+
+ describe "#process days per year when joining date is current year" do
+ before do
+ create(:employment, company:, user:, joined_at: Date.new(Time.current.year, 1, 5), resigned_at: nil)
+ user.add_role :admin, company
+
+ params = {
+ user_id: user.id,
+ year: Date.today.year
+ }
+
+ service = TimeoffEntries::IndexService.new(user, company, params[:user_id], params[:year])
+ @data = service.process
+ end
+
+ it "returns leave balance for days per year" do
+ summary_object = {
+ id: leave_type_days_per_year.id,
+ name: leave_type_days_per_year.name,
+ icon: leave_type_days_per_year.icon,
+ color: leave_type_days_per_year.color,
+ total_leave_type_days: 2,
+ timeoff_entries_duration: 0,
+ net_duration: 960,
+ net_days: 2
+ }
+
+ expect(@data[:leave_balance][3]).to eq(summary_object)
+ end
+ end
+end
diff --git a/spec/services/timeoff_entries/index_service_spec.rb.rb b/spec/services/timeoff_entries/index_service_spec.rb.rb
deleted file mode 100644
index e8f26bd354..0000000000
--- a/spec/services/timeoff_entries/index_service_spec.rb.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require "rails_helper"
-
-RSpec.describe TimeoffEntries::IndexService do # rubocop:disable RSpec/FilePath
- let(:company) { create(:company) }
- let(:user) { create(:user, current_workspace_id: company.id) }
- let!(:leave) { create(:leave, company:, year: Date.today.year) }
- let!(:leave_type) {
- create(
- :leave_type,
- name: "Annual",
- allocation_value: 2,
- allocation_period: :days,
- allocation_frequency: :per_month,
- carry_forward_days: 5,
- leave:
- )
- }
-
- let!(:timeoff_entry) { # rubocop:disable RSpec/LetSetup
- create(
- :timeoff_entry,
- duration: 60,
- leave_date: Date.today,
- user:,
- leave_type:)
- }
-
- describe "#initialize" do
- it "checks preset values in initialize method" do
- params = {
- user_id: user.id,
- year: Date.today.year
- }
-
- service = TimeoffEntries::IndexService.new(params, company, user)
-
- expect(service.params.present?).to be true
- expect(service.current_company.present?).to be true
- expect(service.current_user.present?).to be true
- end
- end
-
- describe "#process" do
- before do
- create(:employment, company:, user:)
- user.add_role :admin, company
-
- params = {
- user_id: user.id,
- year: Date.today.year
- }
-
- service = TimeoffEntries::IndexService.new(params, company, user)
- @data = service.process
- end
-
- it "checks for all timeoff entries of current user" do
- timeoff_entries ||= company.timeoff_entries.includes([:leave_type])
- .where(user_id: user.id)
- .order(leave_date: :desc)
-
- expect(@data[:timeoff_entries]).to eq(timeoff_entries)
- end
-
- it "returns all the employees if the current user is admin" do
- expect(@data[:employees]).to eq(company.users)
- end
-
- it "returns total timeoff entries duration" do
- timeoff_entries_duration ||= company.timeoff_entries.includes([:leave_type])
- .where(user_id: user.id)
- .order(leave_date: :desc)
- .sum(:duration)
-
- expect(@data[:total_timeoff_entries_duration]).to eq(timeoff_entries_duration)
- end
-
- it "returns leave balance" do
- summary_object = {
- id: leave_type.id,
- name: leave_type.name,
- icon: leave_type.icon,
- color: leave_type.color,
- total_leave_type_days: 24,
- timeoff_entries_duration: 60,
- net_duration: 11460,
- net_days: 11460 / 60 / 8
- }
-
- expect(@data[:leave_balance][0]).to eq(summary_object)
- end
- end
-end
diff --git a/tailwind.config.js b/tailwind.config.js
index 44a5c370ed..bbacf09eb8 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -146,10 +146,6 @@ module.exports = {
800: "#ADA4CE",
50: "#CDD6DF33",
},
- "miru-red": {
- 400: "#E04646",
- 200: "#EB5B5B",
- },
"miru-white": {
1000: "#FFFFFF",
},
@@ -206,6 +202,10 @@ module.exports = {
600: "#3111A6",
400: "#8062EF",
},
+ "miru-red": {
+ 400: "#E04646",
+ 200: "#EB5B5B",
+ },
},
fontFamily: {
manrope: "'Manrope', serif",
diff --git a/yarn.lock b/yarn.lock
index 4e940d1c5b..bd82f37517 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3925,9 +3925,9 @@ flatted@^3.1.0:
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.0.0, follow-redirects@^1.15.0:
- version "1.15.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
- integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3:
version "0.3.3"
|