|
| 1 | +import builtins |
1 | 2 | import uuid |
2 | 3 | from collections.abc import Sequence |
3 | 4 | from datetime import UTC, datetime |
|
15 | 16 | from polar.config import Environment, settings |
16 | 17 | from polar.customer.repository import CustomerRepository |
17 | 18 | from polar.enums import InvoiceNumbering |
18 | | -from polar.exceptions import NotPermitted, PolarError, PolarRequestValidationError |
| 19 | +from polar.exceptions import ( |
| 20 | + NotPermitted, |
| 21 | + PolarError, |
| 22 | + PolarRequestValidationError, |
| 23 | + ValidationError, |
| 24 | +) |
19 | 25 | from polar.integrations.loops.service import loops as loops_service |
20 | 26 | from polar.integrations.plain.service import plain as plain_service |
21 | 27 | from polar.kit.anonymization import anonymize_email_for_deletion, anonymize_for_deletion |
@@ -332,23 +338,96 @@ async def update( |
332 | 338 | }, |
333 | 339 | ) |
334 | 340 |
|
335 | | - # Only store details once to avoid API overrides later w/o review |
336 | | - # We do allow initial details being set upon creation that will still require review, |
337 | | - # so upon creation we set details but not details_submitted_at |
338 | | - # so details_submitted_at effectively doubles as a "submit for review" |
339 | | - # timestamp, for now. We'll revisit this soon enough. @pieterbeulque |
340 | | - if not organization.details_submitted_at and update_schema.details: |
| 341 | + if update_schema.details: |
341 | 342 | organization.details = cast( |
342 | 343 | OrganizationDetails, update_schema.details.model_dump() |
343 | 344 | ) |
| 345 | + |
| 346 | + organization = await repository.update(organization, update_dict=update_dict) |
| 347 | + |
| 348 | + await self._after_update(session, organization) |
| 349 | + return organization |
| 350 | + |
| 351 | + def _validate_review_submission( |
| 352 | + self, organization: Organization |
| 353 | + ) -> builtins.list[ValidationError]: |
| 354 | + errors: builtins.list[ValidationError] = [] |
| 355 | + |
| 356 | + if not organization.name or not organization.name.strip(): |
| 357 | + errors.append( |
| 358 | + { |
| 359 | + "loc": ("body", "name"), |
| 360 | + "msg": "Organization name is required.", |
| 361 | + "type": "value_error", |
| 362 | + "input": organization.name, |
| 363 | + } |
| 364 | + ) |
| 365 | + |
| 366 | + if not organization.website: |
| 367 | + errors.append( |
| 368 | + { |
| 369 | + "loc": ("body", "website"), |
| 370 | + "msg": "Website is required.", |
| 371 | + "type": "value_error", |
| 372 | + "input": organization.website, |
| 373 | + } |
| 374 | + ) |
| 375 | + |
| 376 | + if not organization.email: |
| 377 | + errors.append( |
| 378 | + { |
| 379 | + "loc": ("body", "email"), |
| 380 | + "msg": "Support email is required.", |
| 381 | + "type": "value_error", |
| 382 | + "input": organization.email, |
| 383 | + } |
| 384 | + ) |
| 385 | + |
| 386 | + if not any( |
| 387 | + social.get("url", "").strip() for social in (organization.socials or []) |
| 388 | + ): |
| 389 | + errors.append( |
| 390 | + { |
| 391 | + "loc": ("body", "socials"), |
| 392 | + "msg": "At least one social media link is required.", |
| 393 | + "type": "value_error", |
| 394 | + "input": organization.socials, |
| 395 | + } |
| 396 | + ) |
| 397 | + |
| 398 | + product_description = (organization.details or {}).get("product_description") |
| 399 | + if ( |
| 400 | + not isinstance(product_description, str) |
| 401 | + or len(product_description.strip()) < 30 |
| 402 | + ): |
| 403 | + errors.append( |
| 404 | + { |
| 405 | + "loc": ("body", "details", "product_description"), |
| 406 | + "msg": "Please provide at least 30 characters.", |
| 407 | + "type": "value_error", |
| 408 | + "input": product_description, |
| 409 | + } |
| 410 | + ) |
| 411 | + |
| 412 | + return errors |
| 413 | + |
| 414 | + async def submit_for_review( |
| 415 | + self, session: AsyncSession, organization: Organization |
| 416 | + ) -> Organization: |
| 417 | + errors = self._validate_review_submission(organization) |
| 418 | + |
| 419 | + if errors: |
| 420 | + raise PolarRequestValidationError(errors) |
| 421 | + |
| 422 | + if organization.details_submitted_at is None: |
344 | 423 | organization.details_submitted_at = datetime.now(UTC) |
345 | 424 | enqueue_job( |
346 | 425 | "organization_review.run_agent", |
347 | 426 | organization_id=organization.id, |
348 | 427 | context=ReviewContext.SUBMISSION, |
349 | 428 | ) |
350 | 429 |
|
351 | | - organization = await repository.update(organization, update_dict=update_dict) |
| 430 | + session.add(organization) |
352 | 431 |
|
353 | 432 | await self._after_update(session, organization) |
354 | 433 | return organization |
@@ -949,8 +1028,8 @@ async def get_ai_review( |
949 | 1028 | ) -> OrganizationReview | None: |
950 | 1029 | """Get the existing AI review for an organization, if any. |
951 | 1030 |
|
952 | | - The actual AI review is now triggered asynchronously via a background |
953 | | - task when organization details are first submitted (see update()). |
| 1031 | + The actual AI review is triggered asynchronously via a background |
| 1032 | + task when organization details are submitted for review. |
954 | 1033 | """ |
955 | 1034 | repository = OrganizationReviewRepository.from_session(session) |
956 | 1035 | return await repository.get_by_organization(organization.id) |
|
0 commit comments