@@ -10,6 +10,7 @@ final class ProductFormRemoteActionUseCase {
1010 }
1111 typealias AddProductCompletion = ( _ result: Result < ResultData , ProductUpdateError > ) -> Void
1212 typealias EditProductCompletion = ( _ productResult: Result < ResultData , ProductUpdateError > ) -> Void
13+ typealias DuplicateProductCompletion = ( _ result: Result < ResultData , ProductUpdateError > ) -> Void
1314
1415 private let stores : StoresManager
1516
@@ -24,28 +25,80 @@ final class ProductFormRemoteActionUseCase {
2425 /// - onCompletion: Called when the remote process finishes.
2526 func addProduct( product: EditableProductModel ,
2627 password: String ? ,
28+ successEventName: WooAnalyticsStat = . addProductSuccess,
29+ failureEventName: WooAnalyticsStat = . addProductFailed,
2730 onCompletion: @escaping AddProductCompletion ) {
2831 addProductRemotely ( product: product) { productResult in
2932 switch productResult {
3033 case . failure( let error) :
31- ServiceLocator . analytics. track ( . addProductFailed , withError: error)
34+ ServiceLocator . analytics. track ( failureEventName , withError: error)
3235 onCompletion ( . failure( error) )
3336 case . success( let product) :
3437 // `self` is retained because the use case is not usually strongly held.
3538 self . updatePasswordRemotely ( product: product, password: password) { passwordResult in
3639 switch passwordResult {
3740 case . failure( let error) :
38- ServiceLocator . analytics. track ( . addProductFailed , withError: error)
41+ ServiceLocator . analytics. track ( failureEventName , withError: error)
3942 onCompletion ( . failure( . passwordCannotBeUpdated) )
4043 case . success( let password) :
41- ServiceLocator . analytics. track ( . addProductSuccess )
44+ ServiceLocator . analytics. track ( successEventName )
4245 onCompletion ( . success( ResultData ( product: product, password: password) ) )
4346 }
4447 }
4548 }
4649 }
4750 }
4851
52+ /// Adds a copy of the input product remotely. The new product will have an updated name, no SKU and its status will be Draft.
53+ /// - Parameters:
54+ /// - originalProduct: The product to be duplicated remotely.
55+ /// - onCompletion: Called when the remote process finishes.
56+ func duplicateProduct( originalProduct: EditableProductModel ,
57+ password: String ? ,
58+ onCompletion: @escaping DuplicateProductCompletion ) {
59+ let productModelToSave : EditableProductModel = {
60+ let newName = String ( format: Localization . copyProductName, originalProduct. name)
61+ let copiedProduct = originalProduct. product. copy (
62+ productID: 0 ,
63+ name: newName,
64+ statusKey: ProductStatus . draft. rawValue,
65+ sku: . some( nil ) // just resetting SKU to nil for simplicity
66+ )
67+ return EditableProductModel ( product: copiedProduct)
68+ } ( )
69+
70+ let successEventName : WooAnalyticsStat = . duplicateProductSuccess
71+ let failureEventName : WooAnalyticsStat = . duplicateProductFailed
72+
73+ addProduct ( product: productModelToSave,
74+ password: password,
75+ successEventName: successEventName,
76+ failureEventName: failureEventName) { result in
77+ switch result {
78+ case . success( let data) :
79+ guard data. product. productType == . variable else {
80+ return onCompletion ( . success( data) )
81+ }
82+ // `self` is retained because the use case is not usually strongly held.
83+ self . duplicateVariations ( originalProduct. product. variations,
84+ from: originalProduct. productID,
85+ to: data. product,
86+ onCompletion: { result in
87+ switch result {
88+ case . success( let product) :
89+ ServiceLocator . analytics. track ( successEventName)
90+ onCompletion ( . success( ResultData ( product: product, password: data. password) ) )
91+ case . failure( let error) :
92+ ServiceLocator . analytics. track ( failureEventName, withError: error)
93+ onCompletion ( . failure( error) )
94+ }
95+ } )
96+ case . failure( let error) :
97+ onCompletion ( . failure( error) )
98+ }
99+ }
100+ }
101+
49102 /// Edits a product and its password remotely.
50103 /// - Parameters:
51104 /// - product: The product to be updated remotely.
@@ -205,4 +258,96 @@ private extension ProductFormRemoteActionUseCase {
205258 }
206259 stores. dispatch ( passwordUpdateAction)
207260 }
261+
262+ func duplicateVariations( _ variationIDs: [ Int64 ] ,
263+ from oldProductID: Int64 ,
264+ to newProduct: EditableProductModel ,
265+ onCompletion: @escaping ( Result < EditableProductModel , ProductUpdateError > ) -> Void ) {
266+ Task { [ weak self] in
267+ guard let self = self else { return }
268+ // Retrieves and duplicate product variations
269+ await withTaskGroup ( of: Void . self, body: { group in
270+ for id in variationIDs {
271+ group. addTask {
272+ guard let variation = await self . retrieveProductVariation ( variationID: id, siteID: newProduct. siteID, productID: oldProductID) else {
273+ return
274+ }
275+ let newVariation = CreateProductVariation ( regularPrice: variation. regularPrice ?? " " , attributes: variation. attributes)
276+ await self . duplicateProductVariation ( newVariation, parent: newProduct)
277+ }
278+ }
279+ } )
280+
281+ // Fetches the updated product and return
282+ do {
283+ let productModel = try await retrieveProduct ( id: newProduct. productID, siteID: newProduct. siteID)
284+ await MainActor . run {
285+ let updatedProduct = EditableProductModel ( product: productModel)
286+ onCompletion ( . success( updatedProduct) )
287+ }
288+ } catch let error {
289+ await MainActor . run {
290+ onCompletion ( . failure( . unknown( error: AnyError ( error) ) ) )
291+ }
292+ }
293+ }
294+ }
295+
296+ func retrieveProduct( id: Int64 , siteID: Int64 ) async throws -> Product {
297+ try await withCheckedThrowingContinuation { [ weak self] continuation in
298+ let action = ProductAction . retrieveProduct ( siteID: siteID, productID: id) { result in
299+ switch result {
300+ case . success( let product) :
301+ continuation. resume ( returning: product)
302+ case . failure( let error) :
303+ continuation. resume ( throwing: error)
304+ }
305+ }
306+ DispatchQueue . main. async { [ weak self] in
307+ self ? . stores. dispatch ( action)
308+ }
309+ } as Product
310+ }
311+
312+ func retrieveProductVariation( variationID: Int64 , siteID: Int64 , productID: Int64 ) async -> ProductVariation ? {
313+ await withCheckedContinuation { [ weak self] continuation in
314+ let action = ProductVariationAction . retrieveProductVariation ( siteID: siteID,
315+ productID: productID,
316+ variationID: variationID,
317+ onCompletion: { result in
318+ switch result {
319+ case . success( let variation) :
320+ continuation. resume ( returning: variation)
321+ case . failure:
322+ continuation. resume ( returning: nil )
323+ }
324+ } )
325+ DispatchQueue . main. async { [ weak self] in
326+ self ? . stores. dispatch ( action)
327+ }
328+ } as ProductVariation ?
329+ }
330+
331+ func duplicateProductVariation( _ newVariation: CreateProductVariation , parent: EditableProductModel ) async {
332+ await withCheckedContinuation { [ weak self] continuation in
333+ let createAction = ProductVariationAction . createProductVariation (
334+ siteID: parent. siteID,
335+ productID: parent. productID,
336+ newVariation: newVariation) { result in
337+ continuation. resume ( returning: ( ) )
338+ }
339+ DispatchQueue . main. async { [ weak self] in
340+ self ? . stores. dispatch ( createAction)
341+ }
342+ } as Void
343+ }
344+ }
345+
346+ private extension ProductFormRemoteActionUseCase {
347+ enum Localization {
348+ static let copyProductName = NSLocalizedString (
349+ " %1$@ Copy " ,
350+ comment: " The default name for a duplicated product, with %1$@ being the original name. Reads like: Ramen Copy "
351+ )
352+ }
208353}
0 commit comments