Current section
Files
Jump to
Current section
Files
lib/mix/tasks/accrue.install.ex
defmodule Mix.Tasks.Accrue.Install do
@shortdoc "Install Accrue into a Phoenix application"
@moduledoc """
Generates host-owned Accrue wiring.
## Flags
* `--billable MyApp.Accounts.User`
* `--billing-context MyApp.Billing`
* `--webhook-path /webhooks/stripe`
* `--admin-mount /billing`
* `--admin` / `--no-admin`
* `--sigra` / `--no-sigra`
* `--dry-run`
* `--yes`
* `--non-interactive`
* `--manual`
* `--force`
* `--write-conflicts`
"""
use Mix.Task
@impl Mix.Task
def run(argv) do
loadpaths()
opts = Accrue.Install.Options.parse!(argv)
project = Accrue.Install.Project.discover!(opts)
validate_planned_config!(project)
print_intro(opts)
print_orchestration(project)
report("config docs: #{String.split(config_docs(), "\n") |> hd()}")
print_auth_guidance(project)
report(Accrue.Install.Templates.stripe_test_mode_readiness())
results =
if opts.dry_run or opts.manual or project.manual? do
report("manual: review generated snippets before applying")
print_manual_snippets(project, opts)
[]
else
install(project, opts)
end
print_summary(results, opts, project)
end
defp loadpaths do
Mix.Task.run("loadpaths")
rescue
e in Mix.Error ->
if Exception.message(e) =~ "errors on dependencies" do
Mix.shell().info(
"loadpaths: continuing with available paths; run mix deps.get if deps are missing"
)
else
reraise e, __STACKTRACE__
end
end
defp print_intro(%Accrue.Install.Options{} = opts) do
report("Accrue installer")
report("dependency: {:igniter, \"~> 0.7.9\", runtime: false}")
report("flags: --dry-run --yes --non-interactive --manual --force --write-conflicts")
report("billing_context: #{opts.billing_context}")
report("webhook_path: #{opts.webhook_path}")
report("admin_mount: #{opts.admin_mount}")
if opts.dry_run do
report("dry-run: no files changed")
end
end
defp print_orchestration(project) do
report("Project: #{inspect(project.__struct__)}")
report("Templates: #{inspect(Accrue.Install.Templates)}")
report("Fingerprints: #{inspect(Accrue.Install.Fingerprints)}")
report("Patches: #{inspect(Accrue.Install.Patches)}")
report(
"Stripe test-mode readiness uses STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET with " <>
"sk_test_ / sk_live_ / whsec_ redaction"
)
if project.manual? do
report("manual: project shape requires snippet review")
end
end
defp print_auth_guidance(%{has_sigra?: true}) do
report("Sigra detected: config :accrue, :auth_adapter, Accrue.Integrations.Sigra")
report("Fallback/community path: Accrue.Auth.Default or a community auth adapter")
end
defp print_auth_guidance(_project) do
report("Auth fallback: Accrue.Auth.Default with prod-safety warning")
report("Community auth adapters can implement Accrue.Auth")
end
defp install(project, opts) do
template_results =
project
|> Accrue.Install.Templates.render_all(opts)
|> Enum.map(fn {path, content} ->
{status, reason} =
Accrue.Install.Fingerprints.write(path, content,
force: opts.force,
dry_run: opts.dry_run
)
report("#{status}: #{Path.relative_to_cwd(path)} #{reason}")
{status, path, reason}
end)
patch_results =
project
|> Accrue.Install.Patches.apply(opts)
|> Enum.map(&report_patch_result/1)
template_results ++ patch_results
end
defp print_manual_snippets(project, opts) do
for {name, snippet} <- Accrue.Install.Patches.manual_snippets(project, opts) do
report("#{name}:")
report(snippet)
end
end
defp report_patch_result({status, path, reason}) when status in [:changed, :skipped] do
report("#{status}: #{Path.relative_to_cwd(path)} #{reason}")
{status, path, reason}
end
defp report_patch_result({:manual, nil, reason, snippet}) do
report("manual: #{reason}")
report(snippet)
{:manual, nil, reason}
end
defp report_patch_result({:manual, path, reason, snippet}) do
report("manual: #{Path.relative_to_cwd(path)} #{reason}")
report(snippet)
{:manual, path, reason}
end
defp print_summary(results, opts, project) do
changed = Enum.count(results, &match?({:changed, _, _}, &1))
skipped = Enum.count(results, &match?({:skipped, _, _}, &1))
manual = if opts.manual or opts.dry_run or project.manual?, do: 1, else: 0
report("changed: #{changed}")
report("skipped: #{skipped}")
report("manual: #{manual}")
end
defp validate_planned_config!(project) do
Accrue.Config.validate!(
repo: project.repo || Module.concat([project.app_module, Repo]),
processor: Accrue.Processor.Stripe,
auth_adapter:
if(project.has_sigra?, do: Accrue.Integrations.Sigra, else: Accrue.Auth.Default),
stripe_secret_key: "sk_test_install_validation",
branding: [
from_email: "billing@example.com",
support_email: "support@example.com"
]
)
end
defp config_docs do
NimbleOptions.docs(Accrue.Config.schema())
end
defp redact(message) do
message
|> to_string()
|> String.replace(~r/sk_(test|live)_[A-Za-z0-9_=-]+/, "sk_\\1_[REDACTED]")
|> String.replace(~r/whsec_[A-Za-z0-9_=-]+/, "whsec_[REDACTED]")
|> String.replace(~r/([A-Z0-9_]*(?:SECRET|KEY)[A-Z0-9_]*=)[^\s,}]+/, "\\1[REDACTED]")
end
defp report(message), do: IO.puts(redact(message))
end